limit transaction size config (#2087)
This commit is contained in:
parent
b64e982ec3
commit
3189819dd5
|
@ -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."
|
|
@ -80,6 +80,7 @@ public class DaoConfig {
|
||||||
* @see #setMaximumSearchResultCountInTransaction(Integer)
|
* @see #setMaximumSearchResultCountInTransaction(Integer)
|
||||||
*/
|
*/
|
||||||
private static final Integer DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION = null;
|
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 Logger ourLog = LoggerFactory.getLogger(DaoConfig.class);
|
||||||
private static final int DEFAULT_EXPUNGE_BATCH_SIZE = 800;
|
private static final int DEFAULT_EXPUNGE_BATCH_SIZE = 800;
|
||||||
private IndexEnabledEnum myIndexMissingFieldsEnabled = IndexEnabledEnum.DISABLED;
|
private IndexEnabledEnum myIndexMissingFieldsEnabled = IndexEnabledEnum.DISABLED;
|
||||||
|
@ -126,6 +127,8 @@ public class DaoConfig {
|
||||||
private boolean myIndexContainedResources = true;
|
private boolean myIndexContainedResources = true;
|
||||||
private int myMaximumExpansionSize = DEFAULT_MAX_EXPANSION_SIZE;
|
private int myMaximumExpansionSize = DEFAULT_MAX_EXPANSION_SIZE;
|
||||||
private Integer myMaximumSearchResultCountInTransaction = DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION;
|
private Integer myMaximumSearchResultCountInTransaction = DEFAULT_MAXIMUM_SEARCH_RESULT_COUNT_IN_TRANSACTION;
|
||||||
|
|
||||||
|
private Integer myMaximumTransactionBundleSize = DEFAULT_MAXIMUM_TRANSACTION_BUNDLE_SIZE;
|
||||||
private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC;
|
private ResourceEncodingEnum myResourceEncoding = ResourceEncodingEnum.JSONC;
|
||||||
/**
|
/**
|
||||||
* update setter javadoc if default changes
|
* update setter javadoc if default changes
|
||||||
|
@ -655,6 +658,31 @@ public class DaoConfig {
|
||||||
myMaximumSearchResultCountInTransaction = theMaximumSearchResultCountInTransaction;
|
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
|
* This setting controls the number of threads allocated to resource reindexing
|
||||||
* (which is only ever used if SearchParameters change, or a manual reindex is
|
* (which is only ever used if SearchParameters change, or a manual reindex is
|
||||||
|
|
|
@ -25,6 +25,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||||
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
|
||||||
import ca.uhn.fhir.interceptor.api.Pointcut;
|
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.DaoRegistry;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
|
||||||
import ca.uhn.fhir.jpa.api.dao.IJpaDao;
|
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.InternalErrorException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.NotModifiedException;
|
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.interceptor.IServerInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
|
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
|
||||||
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding;
|
||||||
|
@ -124,6 +126,8 @@ public abstract class BaseTransactionProcessor {
|
||||||
private MatchResourceUrlService myMatchResourceUrlService;
|
private MatchResourceUrlService myMatchResourceUrlService;
|
||||||
@Autowired
|
@Autowired
|
||||||
private HapiTransactionService myHapiTransactionService;
|
private HapiTransactionService myHapiTransactionService;
|
||||||
|
@Autowired
|
||||||
|
private DaoConfig myDaoConfig;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void start() {
|
public void start() {
|
||||||
|
@ -342,7 +346,15 @@ public abstract class BaseTransactionProcessor {
|
||||||
throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + transactionType);
|
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 TransactionDetails transactionDetails = new TransactionDetails();
|
||||||
final StopWatch transactionStopWatch = new StopWatch();
|
final StopWatch transactionStopWatch = new StopWatch();
|
||||||
|
@ -350,7 +362,7 @@ public abstract class BaseTransactionProcessor {
|
||||||
List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest);
|
List<IBase> requestEntries = myVersionAdapter.getEntries(theRequest);
|
||||||
|
|
||||||
// Do all entries have a verb?
|
// 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);
|
IBase nextReqEntry = requestEntries.get(i);
|
||||||
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
|
String verb = myVersionAdapter.getEntryRequestVerb(myContext, nextReqEntry);
|
||||||
if (verb == null || !isValidVerb(verb)) {
|
if (verb == null || !isValidVerb(verb)) {
|
||||||
|
|
|
@ -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
|
|
||||||
*/
|
|
||||||
}
|
|
|
@ -2888,108 +2888,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest {
|
||||||
assertEquals(1, found.size().intValue());
|
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
|
@Test
|
||||||
public void testTransactionWithRelativeOidIds() {
|
public void testTransactionWithRelativeOidIds() {
|
||||||
Bundle res = new Bundle();
|
Bundle res = new Bundle();
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue