limit transaction size config (#2087)

This commit is contained in:
Ken Stevens 2020-09-16 14:13:03 -04:00 committed by GitHub
parent b64e982ec3
commit 3189819dd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 125 additions and 150 deletions

View File

@ -0,0 +1,5 @@
---
type: add
issue: 2087
title: "Added new DaoConfig parameter called maximumTransactionBundleSize that if not-null will throw a
PayloadTooLarge exception when the number of resources in a transaction bundle exceeds this size."

View File

@ -80,6 +80,7 @@ public class DaoConfig {
* @see #setMaximumSearchResultCountInTransaction(Integer)
*/
private static final Integer DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION = null;
private static final Integer DEFAULT_MAXIMUM_TRANSACTION_BUNDLE_SIZE = null;
private static final Logger ourLog = LoggerFactory.getLogger(DaoConfig.class);
private static final int DEFAULT_EXPUNGE_BATCH_SIZE = 800;
private IndexEnabledEnum myIndexMissingFieldsEnabled = IndexEnabledEnum.DISABLED;
@ -126,6 +127,8 @@ public class DaoConfig {
private boolean myIndexContainedResources = true;
private int myMaximumExpansionSize = DEFAULT_MAX_EXPANSION_SIZE;
private Integer myMaximumSearchResultCountInTransaction = DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION;
private Integer myMaximumTransactionBundleSize = DEFAULT_MAXIMUM_TRANSACTION_BUNDLE_SIZE;
private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC;
/**
* update setter javadoc if default changes
@ -655,6 +658,31 @@ public class DaoConfig {
myMaximumSearchResultCountInTransaction = theMaximumSearchResultCountInTransaction;
}
/**
* Specifies the maximum number of resources permitted within a single transaction bundle.
* If a transaction bundle is submitted with more than this number of resources, it will be
* rejected with a PayloadTooLarge exception.
* <p>
* The default value is <code>null</code>, which means that there is no limit.
* </p>
*/
public Integer getMaximumTransactionBundleSize() {
return myMaximumTransactionBundleSize;
}
/**
* Specifies the maximum number of resources permitted within a single transaction bundle.
* If a transaction bundle is submitted with more than this number of resources, it will be
* rejected with a PayloadTooLarge exception.
* <p>
* The default value is <code>null</code>, which means that there is no limit.
* </p>
*/
public DaoConfig setMaximumTransactionBundleSize(Integer theMaximumTransactionBundleSize) {
myMaximumTransactionBundleSize = theMaximumTransactionBundleSize;
return this;
}
/**
* This setting controls the number of threads allocated to resource reindexing
* (which is only ever used if SearchParameters change, or a manual reindex is

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.IJpaDao;
@ -54,6 +55,7 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
@ -124,6 +126,8 @@ public abstract class BaseTransactionProcessor {
private MatchResourceUrlService myMatchResourceUrlService;
@Autowired
private HapiTransactionService myHapiTransactionService;
@Autowired
private DaoConfig myDaoConfig;
@PostConstruct
public void start() {
@ -342,7 +346,15 @@ public abstract class BaseTransactionProcessor {
throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType);
}
ourLog.debug("Beginning {} with {} resources", theActionName, myVersionAdapter.getEntries(theRequest).size());
int numberOfEntries = myVersionAdapter.getEntries(theRequest).size();
if (myDaoConfig.getMaximumTransactionBundleSize() != null && numberOfEntries > myDaoConfig.getMaximumTransactionBundleSize()) {
throw new PayloadTooLargeException("Transaction Bundle Too large. Transaction bundle contains " +
numberOfEntries +
" which exceedes the maximum permitted transaction bundle size of " + myDaoConfig.getMaximumTransactionBundleSize());
}
ourLog.debug("Beginning {} with {} resources", theActionName, numberOfEntries);
final TransactionDetails transactionDetails = new TransactionDetails();
final StopWatch transactionStopWatch = new StopWatch();
@ -350,7 +362,7 @@ public abstract class BaseTransactionProcessor {
List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest);
// Do all entries have a verb?
for (int i = 0; i < myVersionAdapter.getEntries(theRequest).size(); i++) {
for (int i = 0; i < numberOfEntries; i++) {
IBase nextReqEntry = requestEntries.get(i);
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
if (verb == null || !isValidVerb(verb)) {

View File

@ -1,46 +0,0 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import ca.uhn.fhir.util.TestUtil;
public class FhirSystemDaoDstu3SearchTest extends BaseJpaDstu3SystemTest {
@Test
public void testSearchByParans() {
// code to come.. just here to prevent a failure
}
/*//@formatter:off
* [ERROR] Search parameter action has conflicting types token and reference
* [ERROR] Search parameter source has conflicting types token and reference
* [ERROR] Search parameter plan has conflicting types reference and token
* [ERROR] Search parameter version has conflicting types token and string
* [ERROR] Search parameter source has conflicting types reference and uri
* [ERROR] Search parameter location has conflicting types reference and uri
* [ERROR] Search parameter title has conflicting types string and token
* [ERROR] Search parameter manufacturer has conflicting types string and reference
* [ERROR] Search parameter address has conflicting types token and string
* [ERROR] Search parameter source has conflicting types reference and string
* [ERROR] Search parameter destination has conflicting types reference and string
* [ERROR] Search parameter responsible has conflicting types reference and string
* [ERROR] Search parameter value has conflicting types token and string
* [ERROR] Search parameter address has conflicting types token and string
* [ERROR] Search parameter address has conflicting types token and string
* [ERROR] Search parameter address has conflicting types token and string
* [ERROR] Search parameter address has conflicting types token and string
* [ERROR] Search parameter action has conflicting types reference and token
* [ERROR] Search parameter version has conflicting types token and string
* [ERROR] Search parameter address has conflicting types token and string
* [ERROR] Search parameter base has conflicting types reference and token
* [ERROR] Search parameter target has conflicting types reference and token
* [ERROR] Search parameter base has conflicting types reference and uri
* [ERROR] Search parameter contact has conflicting types string and token
* [ERROR] Search parameter substance has conflicting types token and reference
* [ERROR] Search parameter provider has conflicting types reference and token
* [ERROR] Search parameter system has conflicting types token and uri
* [ERROR] Search parameter reference has conflicting types reference and uri
* //@formatter:off
*/
}

View File

@ -2888,108 +2888,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
assertEquals(1, found.size().intValue());
}
//
//
// /**
// * Issue #55
// */
// @Test
// public void testTransactionWithCidIds() throws Exception {
// Bundle request = new Bundle();
//
// Patient p1 = new Patient();
// p1.setId("cid:patient1");
// p1.addIdentifier().setSystem("system").setValue("testTransactionWithCidIds01");
// res.add(p1);
//
// Observation o1 = new Observation();
// o1.setId("cid:observation1");
// o1.getIdentifier().setSystem("system").setValue("testTransactionWithCidIds02");
// o1.setSubject(new Reference("Patient/cid:patient1"));
// res.add(o1);
//
// Observation o2 = new Observation();
// o2.setId("cid:observation2");
// o2.getIdentifier().setSystem("system").setValue("testTransactionWithCidIds03");
// o2.setSubject(new Reference("Patient/cid:patient1"));
// res.add(o2);
//
// ourSystemDao.transaction(res);
//
// assertTrue(p1.getId().getValue(), p1.getId().getIdPart().matches("^[0-9]+$"));
// assertTrue(o1.getId().getValue(), o1.getId().getIdPart().matches("^[0-9]+$"));
// assertTrue(o2.getId().getValue(), o2.getId().getIdPart().matches("^[0-9]+$"));
//
// assertThat(o1.getSubject().getReference().getValue(), endsWith("Patient/" + p1.getId().getIdPart()));
// assertThat(o2.getSubject().getReference().getValue(), endsWith("Patient/" + p1.getId().getIdPart()));
//
// }
//
// @Test
// public void testTransactionWithDelete() throws Exception {
// Bundle request = new Bundle();
//
// /*
// * Create 3
// */
//
// List<IResource> res;
// res = new ArrayList<IResource>();
//
// Patient p1 = new Patient();
// p1.addIdentifier().setSystem("urn:system").setValue("testTransactionWithDelete");
// res.add(p1);
//
// Patient p2 = new Patient();
// p2.addIdentifier().setSystem("urn:system").setValue("testTransactionWithDelete");
// res.add(p2);
//
// Patient p3 = new Patient();
// p3.addIdentifier().setSystem("urn:system").setValue("testTransactionWithDelete");
// res.add(p3);
//
// ourSystemDao.transaction(res);
//
// /*
// * Verify
// */
//
// IBundleProvider results = ourPatientDao.search(Patient.SP_IDENTIFIER, new TokenParam("urn:system",
// "testTransactionWithDelete"));
// assertEquals(3, results.size());
//
// /*
// * Now delete 2
// */
//
// request = new Bundle();
// res = new ArrayList<IResource>();
// List<IResource> existing = results.getResources(0, 3);
//
// p1 = new Patient();
// p1.setId(existing.get(0).getId());
// ResourceMetadataKeyEnum.DELETED_AT.put(p1, InstantDt.withCurrentTime());
// res.add(p1);
//
// p2 = new Patient();
// p2.setId(existing.get(1).getId());
// ResourceMetadataKeyEnum.DELETED_AT.put(p2, InstantDt.withCurrentTime());
// res.add(p2);
//
// ourSystemDao.transaction(res);
//
// /*
// * Verify
// */
//
// IBundleProvider results2 = ourPatientDao.search(Patient.SP_IDENTIFIER, new TokenParam("urn:system",
// "testTransactionWithDelete"));
// assertEquals(1, results2.size());
// List<IResource> existing2 = results2.getResources(0, 1);
// assertEquals(existing2.get(0).getId(), existing.get(2).getId());
//
// }
@Test
public void testTransactionWithRelativeOidIds() {
Bundle res = new Bundle();

View File

@ -0,0 +1,78 @@
package ca.uhn.fhir.jpa.dao.dstu3;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.rest.server.exceptions.PayloadTooLargeException;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb;
import org.hl7.fhir.dstu3.model.Observation;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
public class FhirSystemDaoTransactionDstu3Test extends BaseJpaDstu3SystemTest {
public static final int TEST_MAXIMUM_TRANSACTION_BUNDLE_SIZE = 5;
@AfterEach
public void after() {
myDaoConfig.setMaximumTransactionBundleSize(new DaoConfig().getMaximumTransactionBundleSize());
}
@BeforeEach
public void beforeDisableResultReuse() {
myDaoConfig.setMaximumTransactionBundleSize(TEST_MAXIMUM_TRANSACTION_BUNDLE_SIZE);
}
private Bundle createInputTransactionWithSize(int theSize) {
Bundle retval = new Bundle();
retval.setType(BundleType.TRANSACTION);
for (int i = 0; i < theSize; ++i) {
Observation obs = new Observation();
obs.setStatus(Observation.ObservationStatus.FINAL);
retval
.addEntry()
.setFullUrl("urn:uuid:000" + i)
.setResource(obs)
.getRequest()
.setMethod(HTTPVerb.POST);
}
return retval;
}
@Test
public void testTransactionTooBig() {
Bundle bundle = createInputTransactionWithSize(TEST_MAXIMUM_TRANSACTION_BUNDLE_SIZE + 1);
try {
mySystemDao.transaction(null, bundle);
fail();
} catch (PayloadTooLargeException e) {
assertThat(e.getMessage(), containsString("Transaction Bundle Too large. Transaction bundle contains " +
(TEST_MAXIMUM_TRANSACTION_BUNDLE_SIZE + 1) +
" which exceedes the maximum permitted transaction bundle size of " + TEST_MAXIMUM_TRANSACTION_BUNDLE_SIZE));
}
}
@Test
public void testTransactionSmallEnough() {
testTransactionBundleSucceedsWithSize(TEST_MAXIMUM_TRANSACTION_BUNDLE_SIZE);
testTransactionBundleSucceedsWithSize(TEST_MAXIMUM_TRANSACTION_BUNDLE_SIZE - 1);
testTransactionBundleSucceedsWithSize(1);
}
private void testTransactionBundleSucceedsWithSize(int theSize) {
Bundle bundle = createInputTransactionWithSize(theSize);
Bundle response = mySystemDao.transaction(null, bundle);
assertEquals(theSize, response.getEntry().size());
assertEquals("201 Created", response.getEntry().get(0).getResponse().getStatus());
assertEquals("201 Created", response.getEntry().get(theSize - 1).getResponse().getStatus());
}
}