From bacd0bfbbb6339c868dcf96068bff6b8c53204c4 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 6 Oct 2017 15:33:24 -0400 Subject: [PATCH] Squashed commit of the following: commit 7ff895de773fe5a56534fa2f34c2b70c4acc3e5a Author: James Agnew Date: Fri Oct 6 15:25:06 2017 -0400 More test fixes commit c9fee23e48f9bdde4948413ea410b75a826c56b7 Author: James Agnew Date: Fri Oct 6 15:14:52 2017 -0400 More tests work commit c796e19458417debf596c84dc007bcec2fbb8229 Author: James Agnew Date: Fri Oct 6 15:00:26 2017 -0400 Get tests passing commit eb2787d30c4152ed0c65b1e21845bffea9d0568c Author: James Agnew Date: Fri Oct 6 14:08:23 2017 -0400 Add an optimistic lock to the ResourceTable commit ff85503acb2fbe352bf3bb43c5e2c0a52bbfbdc7 Author: James Date: Fri Oct 6 08:56:35 2017 -0400 Add a test --- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 12 -- .../uhn/fhir/jpa/entity/BaseHasResource.java | 90 +++++------ .../ca/uhn/fhir/jpa/entity/ResourceTable.java | 43 ++++- .../jpa/sp/SearchParamPresenceSvcImpl.java | 2 +- .../BaseSubscriptionInterceptor.java | 21 ++- .../SubscriptionActivatingSubscriber.java | 23 ++- .../uhn/fhir/jpa/config/TestDstu3Config.java | 54 ++++--- .../ca/uhn/fhir/jpa/config/TestR4Config.java | 4 +- .../FhirResourceDaoDstu2SearchNoFtTest.java | 1 - .../dstu3/FhirDaoConcurrencyDstu3Test.java | 153 ++++++++++++++++++ .../dstu3/FhirResourceDaoDstu3UpdateTest.java | 3 + .../jpa/dao/dstu3/FhirSystemDaoDstu3Test.java | 90 ++++++----- ...hirResourceDaoR4UniqueSearchParamTest.java | 6 +- .../BaseResourceProviderDstu2Test.java | 3 + .../jpa/stresstest/StressTestDstu3Test.java | 77 ++++----- .../EmailSubscriptionDstu2Test.java | 1 + .../subscription/r4/RestHookTestR4Test.java | 4 +- .../derby_maintenance.txt | 20 +++ src/changes/changes.xml | 7 + 19 files changed, 431 insertions(+), 183 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirDaoConcurrencyDstu3Test.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 0a89964524a..4d78e03b187 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -1011,14 +1011,6 @@ public abstract class BaseHapiFhirDao implements IDao { changed = true; } - if (theResource instanceof IResource) { - String title = ResourceMetadataKeyEnum.TITLE.get((IResource) theResource); - if (title != null && title.length() > BaseHasResource.MAX_TITLE_LENGTH) { - title = title.substring(0, BaseHasResource.MAX_TITLE_LENGTH); - } - theEntity.setTitle(title); - } - return changed; } @@ -1052,10 +1044,6 @@ public abstract class BaseHapiFhirDao implements IDao { ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated()); IDao.RESOURCE_PID.put(res, theEntity.getId()); - if (theEntity.getTitle() != null) { - ResourceMetadataKeyEnum.TITLE.put(res, theEntity.getTitle()); - } - Collection tags = theEntity.getTags(); if (theEntity.isHasTags()) { TagList tagList = new TagList(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java index 055ed717096..fe1160c9cbf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java @@ -20,52 +20,54 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import java.util.Collection; -import java.util.Date; - -import javax.persistence.*; - import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; +import org.hibernate.annotations.OptimisticLock; + +import javax.persistence.*; +import java.util.Collection; +import java.util.Date; @MappedSuperclass public abstract class BaseHasResource { - public static final int MAX_TITLE_LENGTH = 100; - @Column(name = "RES_DELETED_AT", nullable = true) @Temporal(TemporalType.TIMESTAMP) private Date myDeleted; @Column(name = "RES_ENCODING", nullable = false, length = 5) @Enumerated(EnumType.STRING) + @OptimisticLock(excluded = true) private ResourceEncodingEnum myEncoding; @Column(name = "RES_VERSION", nullable = true, length = 7) @Enumerated(EnumType.STRING) + @OptimisticLock(excluded = true) private FhirVersionEnum myFhirVersion; @OneToOne(optional = true, fetch = FetchType.EAGER, cascade = {}, orphanRemoval = false) @JoinColumn(name = "FORCED_ID_PID") + @OptimisticLock(excluded = true) private ForcedId myForcedId; @Column(name = "HAS_TAGS", nullable = false) + @OptimisticLock(excluded = true) private boolean myHasTags; @Temporal(TemporalType.TIMESTAMP) @Column(name = "RES_PUBLISHED", nullable = false) + @OptimisticLock(excluded = true) private Date myPublished; @Column(name = "RES_TEXT", length = Integer.MAX_VALUE - 1, nullable = false) @Lob() + @OptimisticLock(excluded = true) private byte[] myResource; - @Column(name = "RES_TITLE", nullable = true, length = MAX_TITLE_LENGTH) - private String myTitle; - @Temporal(TemporalType.TIMESTAMP) @Column(name = "RES_UPDATED", nullable = false) + @OptimisticLock(excluded = true) private Date myUpdated; public abstract BaseTag addTag(TagDefinition theDef); @@ -74,18 +76,36 @@ public abstract class BaseHasResource { return myDeleted; } + public void setDeleted(Date theDate) { + myDeleted = theDate; + } + public ResourceEncodingEnum getEncoding() { return myEncoding; } + public void setEncoding(ResourceEncodingEnum theEncoding) { + myEncoding = theEncoding; + } + public FhirVersionEnum getFhirVersion() { return myFhirVersion; } + public void setFhirVersion(FhirVersionEnum theFhirVersion) { + myFhirVersion = theFhirVersion; + } + public ForcedId getForcedId() { return myForcedId; } + public void setForcedId(ForcedId theForcedId) { + myForcedId = theForcedId; + } + + public abstract Long getId(); + public abstract IdDt getIdDt(); public InstantDt getPublished() { @@ -96,22 +116,30 @@ public abstract class BaseHasResource { } } + public void setPublished(InstantDt thePublished) { + myPublished = thePublished.getValue(); + } + public byte[] getResource() { return myResource; } + public void setResource(byte[] theResource) { + myResource = theResource; + } + public abstract String getResourceType(); public abstract Collection getTags(); - public String getTitle() { - return myTitle; - } - public InstantDt getUpdated() { return new InstantDt(myUpdated); } + public void setUpdated(InstantDt theUpdated) { + myUpdated = theUpdated.getValue(); + } + public Date getUpdatedDate() { return myUpdated; } @@ -122,24 +150,6 @@ public abstract class BaseHasResource { return myHasTags; } - public void setDeleted(Date theDate) { - myDeleted = theDate; - } - - public abstract Long getId(); - - public void setEncoding(ResourceEncodingEnum theEncoding) { - myEncoding = theEncoding; - } - - public void setFhirVersion(FhirVersionEnum theFhirVersion) { - myFhirVersion = theFhirVersion; - } - - public void setForcedId(ForcedId theForcedId) { - myForcedId = theForcedId; - } - public void setHasTags(boolean theHasTags) { myHasTags = theHasTags; } @@ -148,24 +158,8 @@ public abstract class BaseHasResource { myPublished = thePublished; } - public void setPublished(InstantDt thePublished) { - myPublished = thePublished.getValue(); - } - - public void setResource(byte[] theResource) { - myResource = theResource; - } - - public void setTitle(String theTitle) { - myTitle = theTitle; - } - public void setUpdated(Date theUpdated) { myUpdated = theUpdated; } - public void setUpdated(InstantDt theUpdated) { - myUpdated = theUpdated.getValue(); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java index d961168f0ab..4df1d50cfd1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java @@ -37,6 +37,7 @@ import org.apache.lucene.analysis.snowball.SnowballPorterFilterFactory; import org.apache.lucene.analysis.standard.StandardFilterFactory; import org.apache.lucene.analysis.standard.StandardTokenizerFactory; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.OptimisticLock; import org.hibernate.search.annotations.*; import org.hibernate.search.annotations.Parameter; @@ -124,12 +125,15 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Field(name = "myContentTextNGram", index = org.hibernate.search.annotations.Index.YES, store = Store.NO, analyze = Analyze.YES, analyzer = @Analyzer(definition = "autocompleteNGramAnalyzer")), @Field(name = "myContentTextPhonetic", index = org.hibernate.search.annotations.Index.YES, store = Store.NO, analyze = Analyze.YES, analyzer = @Analyzer(definition = "autocompletePhoneticAnalyzer")) }) + @OptimisticLock(excluded = true) private String myContentText; @Column(name = "HASH_SHA256", length = 64, nullable = true) + @OptimisticLock(excluded = true) private String myHashSha256; @Column(name = "SP_HAS_LINKS") + @OptimisticLock(excluded = true) private boolean myHasLinks; @Id @@ -139,12 +143,15 @@ public class ResourceTable extends BaseHasResource implements Serializable { private Long myId; @OneToMany(mappedBy = "myTargetResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) private Collection myIncomingResourceLinks; @Column(name = "SP_INDEX_STATUS", nullable = true) + @OptimisticLock(excluded = true) private Long myIndexStatus; @Column(name = "RES_LANGUAGE", length = MAX_LANGUAGE_LENGTH, nullable = true) + @OptimisticLock(excluded = true) private String myLanguage; /** @@ -157,69 +164,100 @@ public class ResourceTable extends BaseHasResource implements Serializable { @Field(name = "myNarrativeTextNGram", index = org.hibernate.search.annotations.Index.YES, store = Store.NO, analyze = Analyze.YES, analyzer = @Analyzer(definition = "autocompleteNGramAnalyzer")), @Field(name = "myNarrativeTextPhonetic", index = org.hibernate.search.annotations.Index.YES, store = Store.NO, analyze = Analyze.YES, analyzer = @Analyzer(definition = "autocompletePhoneticAnalyzer")) }) + @OptimisticLock(excluded = true) private String myNarrativeText; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) private Collection myParamsCoords; @Column(name = "SP_COORDS_PRESENT") + @OptimisticLock(excluded = true) private boolean myParamsCoordsPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) private Collection myParamsDate; @Column(name = "SP_DATE_PRESENT") + @OptimisticLock(excluded = true) private boolean myParamsDatePopulated; + @OptimisticLock(excluded = true) @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) private Collection myParamsNumber; @Column(name = "SP_NUMBER_PRESENT") + @OptimisticLock(excluded = true) private boolean myParamsNumberPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) private Collection myParamsQuantity; @Column(name = "SP_QUANTITY_PRESENT") + @OptimisticLock(excluded = true) private boolean myParamsQuantityPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) private Collection myParamsString; @Column(name = "SP_STRING_PRESENT") + @OptimisticLock(excluded = true) private boolean myParamsStringPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) private Collection myParamsToken; @Column(name = "SP_TOKEN_PRESENT") + @OptimisticLock(excluded = true) private boolean myParamsTokenPopulated; @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) private Collection myParamsUri; @Column(name = "SP_URI_PRESENT") + @OptimisticLock(excluded = true) private boolean myParamsUriPopulated; @Column(name = "RES_PROFILE", length = MAX_PROFILE_LENGTH, nullable = true) + @OptimisticLock(excluded = true) private String myProfile; - @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) - private Collection myParamsCompositeStringUnique; + // Added in 3.0.0 - Should make this a primitive Boolean at some point + @OptimisticLock(excluded = true) @Column(name = "SP_CMPSTR_UNIQ_PRESENT") private Boolean myParamsCompositeStringUniquePresent = false; + + @OneToMany(mappedBy = "myResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) + @OptimisticLock(excluded = true) + private Collection myParamsCompositeStringUnique; + @OneToMany(mappedBy = "mySourceResource", cascade = {}, fetch = FetchType.LAZY, orphanRemoval = false) @IndexedEmbedded() + @OptimisticLock(excluded = true) private Collection myResourceLinks; + @Column(name = "RES_TYPE", length = RESTYPE_LEN) @Field + @OptimisticLock(excluded = true) private String myResourceType; + @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + @OptimisticLock(excluded = true) private Collection mySearchParamPresents; + @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true) + @OptimisticLock(excluded = true) private Set myTags; + @Transient private transient boolean myUnchangedInCurrentOperation; + + @Version @Column(name = "RES_VER") private long myVersion; @@ -555,7 +593,6 @@ public class ResourceTable extends BaseHasResource implements Serializable { retVal.setResourceType(myResourceType); retVal.setVersion(myVersion); - retVal.setTitle(getTitle()); retVal.setPublished(getPublished()); retVal.setUpdated(getUpdated()); retVal.setEncoding(getEncoding()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java index 41b9df3e3ab..667bf38d859 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/sp/SearchParamPresenceSvcImpl.java @@ -89,7 +89,7 @@ public class SearchParamPresenceSvcImpl implements ISearchParamPresenceSvc { searchParam = new SearchParam(); searchParam.setResourceName(resourceType); searchParam.setParamName(paramName); - searchParam = mySearchParamDao.saveAndFlush(searchParam); + searchParam = mySearchParamDao.save(searchParam); ourLog.info("Added search param {} with pid {}", paramName, searchParam.getId()); // Don't add the newly saved entity to the map in case the save fails } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java index 3f259e6cf77..eb91ba485f3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionInterceptor.java @@ -36,6 +36,7 @@ import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.hl7.fhir.exceptions.FHIRException; @@ -51,8 +52,12 @@ import org.springframework.messaging.MessageHandler; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.support.ExecutorSubscribableChannel; import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -87,6 +92,9 @@ public abstract class BaseSubscriptionInterceptor exten @Autowired(required = false) @Qualifier("myEventDefinitionDaoR4") private IFhirResourceDao myEventDefinitionDaoR4; + @Autowired + private PlatformTransactionManager myTxManager; + /** * Constructor */ @@ -368,6 +376,11 @@ public abstract class BaseSubscriptionInterceptor exten myResourceDaos = theResourceDaos; } + @VisibleForTesting + public void setTxManager(PlatformTransactionManager theTxManager) { + myTxManager = theTxManager; + } + @PostConstruct public void start() { for (IFhirResourceDao next : myResourceDaos) { @@ -452,7 +465,13 @@ public abstract class BaseSubscriptionInterceptor exten registerSubscriptionCheckingSubscriber(); registerDeliverySubscriber(); - initSubscriptions(); + TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager); + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + initSubscriptions(); + } + }); } protected void submitResourceModified(final ResourceModifiedMessage theMsg) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java index 8a37edbb3f7..fd5919eaa75 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/SubscriptionActivatingSubscriber.java @@ -32,6 +32,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.concurrent.ConcurrentHashMap; @@ -53,22 +55,27 @@ public class SubscriptionActivatingSubscriber { myCtx = theSubscriptionDao.getContext(); } - public void activateAndRegisterSubscriptionIfRequired(IBaseResource theSubscription) { + public void activateAndRegisterSubscriptionIfRequired(final IBaseResource theSubscription) { boolean subscriptionTypeApplies = BaseSubscriptionSubscriber.subscriptionTypeApplies(myCtx, theSubscription, myChannelType); if (subscriptionTypeApplies == false) { return; } - IPrimitiveType status = myCtx.newTerser().getSingleValueOrNull(theSubscription, BaseSubscriptionInterceptor.SUBSCRIPTION_STATUS, IPrimitiveType.class); + final IPrimitiveType status = myCtx.newTerser().getSingleValueOrNull(theSubscription, BaseSubscriptionInterceptor.SUBSCRIPTION_STATUS, IPrimitiveType.class); String statusString = status.getValueAsString(); - String requestedStatus = Subscription.SubscriptionStatus.REQUESTED.toCode(); - String activeStatus = Subscription.SubscriptionStatus.ACTIVE.toCode(); + final String requestedStatus = Subscription.SubscriptionStatus.REQUESTED.toCode(); + final String activeStatus = Subscription.SubscriptionStatus.ACTIVE.toCode(); if (requestedStatus.equals(statusString)) { - status.setValueAsString(activeStatus); - ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), requestedStatus, activeStatus); - mySubscriptionDao.update(theSubscription); - mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public void afterCommit() { + status.setValueAsString(activeStatus); + ourLog.info("Activating and registering subscription {} from status {} to {}", theSubscription.getIdElement().toUnqualified().getValue(), requestedStatus, activeStatus); + mySubscriptionDao.update(theSubscription); + mySubscriptionInterceptor.registerSubscription(theSubscription.getIdElement(), theSubscription); + } + }); } else if (activeStatus.equals(statusString)) { if (!mySubscriptionInterceptor.hasSubscription(theSubscription.getIdElement())) { ourLog.info("Registering active subscription {}", theSubscription.getIdElement().toUnqualified().getValue()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java index 970cbdc37fd..c827bb7d27e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestDstu3Config.java @@ -8,6 +8,7 @@ import java.util.concurrent.TimeUnit; import javax.persistence.EntityManagerFactory; import javax.sql.DataSource; +import net.ttddyy.dsproxy.listener.logging.SLF4JLogLevel; import org.apache.commons.dbcp2.BasicDataSource; import org.hibernate.jpa.HibernatePersistenceProvider; import org.springframework.context.annotation.*; @@ -33,9 +34,9 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { public DaoConfig daoConfig() { return new DaoConfig(); } - + @Bean() - public DataSource dataSource() { + public BasicDataSource basicDataSource() { BasicDataSource retVal = new BasicDataSource() { @@ -48,36 +49,36 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { ourLog.error("Exceeded maximum wait for connection", e); logGetConnectionStackTrace(); // if ("true".equals(System.getProperty("ci"))) { - fail("Exceeded maximum wait for connection: "+ e.toString()); + fail("Exceeded maximum wait for connection: "+ e.toString()); // } // System.exit(1); retVal = null; } - + try { throw new Exception(); } catch (Exception e) { myLastStackTrace = e; } - + return retVal; } private void logGetConnectionStackTrace() { - StringBuilder b = new StringBuilder(); - b.append("Last connection request stack trace:"); - for (StackTraceElement next : myLastStackTrace.getStackTrace()) { - b.append("\n "); - b.append(next.getClassName()); - b.append("."); - b.append(next.getMethodName()); - b.append("("); - b.append(next.getFileName()); - b.append(":"); - b.append(next.getLineNumber()); - b.append(")"); - } - ourLog.info(b.toString()); + StringBuilder b = new StringBuilder(); + b.append("Last connection request stack trace:"); + for (StackTraceElement next : myLastStackTrace.getStackTrace()) { + b.append("\n "); + b.append(next.getClassName()); + b.append("."); + b.append(next.getMethodName()); + b.append("("); + b.append(next.getFileName()); + b.append(":"); + b.append(next.getLineNumber()); + b.append(")"); + } + ourLog.info(b.toString()); } }; @@ -92,13 +93,20 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { * and catch any potential deadlocks caused by database connection * starvation */ - int maxThreads = (int) (Math.random() * 6) + 1; + int maxThreads = (int) (Math.random() * 6.0) + 1; retVal.setMaxTotal(maxThreads); + return retVal; + } + + @Bean() + @Primary() + public DataSource dataSource() { + DataSource dataSource = ProxyDataSourceBuilder - .create(retVal) - // .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") - .logSlowQueryBySlf4j(100, TimeUnit.MILLISECONDS) + .create(basicDataSource()) +// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") + .logSlowQueryBySlf4j(1000, TimeUnit.MILLISECONDS) .countQuery() .build(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java index 8794a67ebd8..82b78e27799 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/config/TestR4Config.java @@ -94,12 +94,12 @@ public class TestR4Config extends BaseJavaConfigR4 { * and catch any potential deadlocks caused by database connection * starvation */ - int maxThreads = (int) (Math.random() * 6) + 1; + int maxThreads = (int) (Math.random() * 6.0) + 1; retVal.setMaxTotal(maxThreads); DataSource dataSource = ProxyDataSourceBuilder .create(retVal) - .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") +// .logQueryBySlf4j(SLF4JLogLevel.INFO, "SQL") .logSlowQueryBySlf4j(10, TimeUnit.SECONDS) .countQuery(new ThreadQueryCountHolder()) .build(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java index f0cc7d84364..cadec9a7a90 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirResourceDaoDstu2SearchNoFtTest.java @@ -921,7 +921,6 @@ public class FhirResourceDaoDstu2SearchNoFtTest extends BaseJpaDstu2Test { List patients = toList(myPatientDao.search(params)); assertEquals(1, patients.size()); assertEquals(id1.getIdPart(), patients.get(0).getId().getIdPart()); - assertEquals("P1TITLE", ResourceMetadataKeyEnum.TITLE.get(patients.get(0))); // Given name shouldn't return for family param params = new SearchParameterMap(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirDaoConcurrencyDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirDaoConcurrencyDstu3Test.java new file mode 100644 index 00000000000..d6c27cb0334 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirDaoConcurrencyDstu3Test.java @@ -0,0 +1,153 @@ +package ca.uhn.fhir.jpa.dao.dstu3; + +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.util.StopWatch; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.util.TestUtil; +import com.phloc.commons.compare.ReverseComparator; +import org.apache.commons.dbcp2.BasicDataSource; +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.IdType; +import org.hl7.fhir.dstu3.model.Organization; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.comparator.ComparableComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.*; + +public class FhirDaoConcurrencyDstu3Test extends BaseJpaDstu3SystemTest { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirDaoConcurrencyDstu3Test.class); + + @Autowired + public BasicDataSource myBasicDataSource; + private int myMaxTotal; + + @After + public void afterResetConnectionPool() { + myBasicDataSource.setMaxTotal(myMaxTotal); + } + + @Before + public void beforeSetUpConnectionPool() { + myMaxTotal = myBasicDataSource.getMaxTotal(); + myBasicDataSource.setMaxTotal(5); + } + + @Test + public void testMultipleConcurrentWritesToSameResource() throws InterruptedException { + + ThreadPoolExecutor exec = new ThreadPoolExecutor(10, 10, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); + + final AtomicInteger errors = new AtomicInteger(); + + List futures = new ArrayList<>(); + for (int i = 0; i < 50; i++) { + final Patient p = new Patient(); + p.setId("PID"); + p.setActive(true); + p.setBirthDate(new Date()); + p.addIdentifier().setSystem("foo1"); + p.addIdentifier().setSystem("foo2"); + p.addIdentifier().setSystem("foo3"); + p.addIdentifier().setSystem("foo4"); + p.addName().setFamily("FOO" + i); + p.addName().addGiven("AAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBB1"); + p.addName().addGiven("AAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBB2"); + p.addName().addGiven("AAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBB3"); + p.addName().addGiven("AAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBB4"); + p.addName().addGiven("AAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBB5"); + p.addName().addGiven("AAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBB6"); + + Organization o = new Organization(); + o.setName("ORG" + i); + + final Bundle t = new Bundle(); + t.setType(BundleType.TRANSACTION); + t.addEntry() + .setResource(p) + .getRequest() + .setUrl("Patient/PID") + .setMethod(HTTPVerb.PUT); + t.addEntry() + .setResource(o) + .getRequest() + .setUrl("Organization") + .setMethod(HTTPVerb.POST); + + if (i == 0) { + mySystemDao.transaction(mySrd, t); + } + futures.add(exec.submit(new Runnable() { + @Override + public void run() { + try { + mySystemDao.transaction(mySrd, t); + } catch (Exception e) { + ourLog.error("Failed to update", e); + errors.incrementAndGet(); + } + } + })); + } + + ourLog.info("Shutting down excutor"); + StopWatch sw = new StopWatch(); + for (Future next : futures) { + while (!next.isDone()) { + Thread.sleep(20); + } + } + exec.shutdown(); + ourLog.info("Shut down excutor in {}ms", sw.getMillis()); + ourLog.info("Had {} errors", errors.get()); + + Patient currentPatient = myPatientDao.read(new IdType("Patient/PID")); + Long currentVersion = currentPatient.getIdElement().getVersionIdPartAsLong(); + ourLog.info("Current version: {}", currentVersion); + + IBundleProvider historyBundle = myPatientDao.history(new IdType("Patient/PID"),null,null,mySrd); + List resources = historyBundle.getResources(0, 1000); + List versions = new ArrayList<>(); + for (IBaseResource next : resources) { + versions.add(next.getIdElement().getVersionIdPartAsLong()); + } + + String message = "Current version is " + currentVersion + " - History is: " + versions; + ourLog.info(message); + + Collections.sort(versions, new ReverseComparator<>(new ComparableComparator())); + Long lastVersion = versions.get(0); + ourLog.info("Last version: {}", lastVersion); + + //assertEquals(message, currentVersion.intValue(), versions.size()); + assertEquals(message, currentVersion, lastVersion); + + } + + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java index b243e95755d..3971b902474 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3UpdateTest.java @@ -252,6 +252,7 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test { p.setId("Patient/A"); String id = myPatientDao.update(p).getId().getValue(); assertThat(id, endsWith("Patient/A/_history/1")); + assertEquals("1", myPatientDao.read(new IdType("Patient/A")).getIdElement().getVersionIdPart()); // Second time should not result in an update p = new Patient(); @@ -259,6 +260,7 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test { p.setId("Patient/A"); id = myPatientDao.update(p).getId().getValue(); assertThat(id, endsWith("Patient/A/_history/1")); + assertEquals("1", myPatientDao.read(new IdType("Patient/A")).getIdElement().getVersionIdPart()); // And third time should not result in an update p = new Patient(); @@ -266,6 +268,7 @@ public class FhirResourceDaoDstu3UpdateTest extends BaseJpaDstu3Test { p.setId("Patient/A"); id = myPatientDao.update(p).getId().getValue(); assertThat(id, endsWith("Patient/A/_history/1")); + assertEquals("1", myPatientDao.read(new IdType("Patient/A")).getIdElement().getVersionIdPart()); myPatientDao.read(new IdType("Patient/A")); myPatientDao.read(new IdType("Patient/A/_history/1")); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index 681cbbf9e4c..8035aabe48e 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -62,43 +62,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { myDaoConfig.setReuseCachedSearchResultsForMillis(null); } - @Test - public void testTransactionWhichFailsPersistsNothing() { - - // Run a transaction which points to that practitioner - // in a field that isn't allowed to refer to a practitioner - Bundle input = new Bundle(); - input.setType(BundleType.TRANSACTION); - - Patient pt = new Patient(); - pt.setId("PT"); - pt.setActive(true); - pt.addName().setFamily("FAMILY"); - input.addEntry() - .setResource(pt) - .getRequest().setMethod(HTTPVerb.PUT).setUrl("Patient/PT"); - - Observation obs = new Observation(); - obs.setId("OBS"); - obs.getCode().addCoding().setSystem("foo").setCode("bar"); - obs.addPerformer().setReference("Practicioner/AAAAA"); - input.addEntry() - .setResource(obs) - .getRequest().setMethod(HTTPVerb.PUT).setUrl("Observation/OBS"); - - try { - mySystemDao.transaction(mySrd, input); - fail(); - } catch (UnprocessableEntityException e) { - assertThat(e.getMessage(), containsString("Resource type 'Practicioner' is not valid for this path")); - } - - assertThat(myResourceTableDao.findAll(), empty()); - assertThat(myResourceIndexedSearchParamStringDao.findAll(), empty()); - - } - - private Bundle createInputTransactionWithPlaceholderIdInMatchUrl(HTTPVerb theVerb) { Patient pat = new Patient(); @@ -209,6 +172,11 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { return null; } + private Bundle loadBundle(String theFileName) throws IOException { + String req = IOUtils.toString(FhirSystemDaoDstu3Test.class.getResourceAsStream(theFileName), StandardCharsets.UTF_8); + return myFhirCtx.newXmlParser().parseResource(Bundle.class, req); + } + @Test public void testBatchCreateWithBadRead() { Bundle request = new Bundle(); @@ -1222,8 +1190,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { @Test public void testTransactionCreateWithPutUsingUrl2() throws Exception { - String req = IOUtils.toString(FhirSystemDaoDstu3Test.class.getResourceAsStream("/bundle-dstu3.xml"), StandardCharsets.UTF_8); - Bundle request = myFhirCtx.newXmlParser().parseResource(Bundle.class, req); + Bundle request = loadBundle("/bundle-dstu3.xml"); mySystemDao.transaction(mySrd, request); } @@ -1702,13 +1669,13 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { //@formatter:off /* * Transaction Order, per the spec: - * + * * Process any DELETE interactions * Process any POST interactions * Process any PUT interactions * Process any GET interactions - * - * This test creates a transaction bundle that includes + * + * This test creates a transaction bundle that includes * these four operations in the reverse order and verifies * that they are invoked correctly. */ @@ -2147,6 +2114,42 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { } } + @Test + public void testTransactionWhichFailsPersistsNothing() { + + // Run a transaction which points to that practitioner + // in a field that isn't allowed to refer to a practitioner + Bundle input = new Bundle(); + input.setType(BundleType.TRANSACTION); + + Patient pt = new Patient(); + pt.setId("PT"); + pt.setActive(true); + pt.addName().setFamily("FAMILY"); + input.addEntry() + .setResource(pt) + .getRequest().setMethod(HTTPVerb.PUT).setUrl("Patient/PT"); + + Observation obs = new Observation(); + obs.setId("OBS"); + obs.getCode().addCoding().setSystem("foo").setCode("bar"); + obs.addPerformer().setReference("Practicioner/AAAAA"); + input.addEntry() + .setResource(obs) + .getRequest().setMethod(HTTPVerb.PUT).setUrl("Observation/OBS"); + + try { + mySystemDao.transaction(mySrd, input); + fail(); + } catch (UnprocessableEntityException e) { + assertThat(e.getMessage(), containsString("Resource type 'Practicioner' is not valid for this path")); + } + + assertThat(myResourceTableDao.findAll(), empty()); + assertThat(myResourceIndexedSearchParamStringDao.findAll(), empty()); + + } + /** * Format changed, source isn't valid */ @@ -2492,7 +2495,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { IdType medOrderId1 = new IdType(outcome.getEntry().get(1).getResponse().getLocation()); /* - * Again! + * Again! */ bundle = new Bundle(); @@ -2815,6 +2818,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java index 8679cd69864..3cf4fcd8aa8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4UniqueSearchParamTest.java @@ -303,7 +303,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { Patient pt1 = new Patient(); pt1.setGender(Enumerations.AdministrativeGender.MALE); pt1.setBirthDateElement(new DateType("2011-01-01")); - IIdType id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless(); + String id1 = myPatientDao.create(pt1).getId().toUnqualifiedVersionless().getValue(); Patient pt2 = new Patient(); pt2.setGender(Enumerations.AdministrativeGender.MALE); @@ -316,7 +316,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { params.add("birthdate", new DateParam("2011-01-01")); IBundleProvider results = myPatientDao.search(params); String searchId = results.getUuid(); - assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1)); assertEquals(SearchBuilder.HandlerTypeEnum.UNIQUE_INDEX, SearchBuilder.getLastHandlerMechanismForUnitTest()); // Other order @@ -326,7 +326,7 @@ public class FhirResourceDaoR4UniqueSearchParamTest extends BaseJpaR4Test { params.add("gender", new TokenParam("http://hl7.org/fhir/administrative-gender", "male")); results = myPatientDao.search(params); assertEquals(searchId, results.getUuid()); - assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1.getValue())); + assertThat(toUnqualifiedVersionlessIdValues(results), containsInAnyOrder(id1)); // Null because we just reuse the last search assertEquals(null, SearchBuilder.getLastHandlerMechanismForUnitTest()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java index b1372bd99e4..b50dd291a99 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java @@ -24,6 +24,7 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import org.springframework.transaction.PlatformTransactionManager; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -47,6 +48,7 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test { protected static GenericWebApplicationContext ourWebApplicationContext; protected static SubscriptionRestHookInterceptor ourRestHookSubscriptionInterceptor; protected static DatabaseBackedPagingProvider ourPagingProvider; + protected static PlatformTransactionManager ourTxManager; public BaseResourceProviderDstu2Test() { super(); @@ -98,6 +100,7 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test { ourWebApplicationContext.refresh(); ourRestHookSubscriptionInterceptor = ourWebApplicationContext.getBean(SubscriptionRestHookInterceptor.class); + ourTxManager = ourWebApplicationContext.getBean(PlatformTransactionManager.class); proxyHandler.getServletContext().setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ourWebApplicationContext); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java index 82d2f6a02fe..ca346af6e6a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/StressTestDstu3Test.java @@ -1,51 +1,54 @@ package ca.uhn.fhir.jpa.stresstest; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import java.util.List; -import java.util.UUID; - +import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator; -import org.hl7.fhir.dstu3.model.*; +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.junit.*; +import org.hl7.fhir.dstu3.model.CodeableConcept; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.Patient; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; -import com.google.common.base.Charsets; -import com.google.common.collect.Lists; +import java.util.List; +import java.util.UUID; -import ca.uhn.fhir.jpa.provider.dstu3.BaseResourceProviderDstu3Test; -import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; -import ca.uhn.fhir.util.TestUtil; +import static org.junit.Assert.*; public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StressTestDstu3Test.class); private RequestValidatingInterceptor myRequestValidatingInterceptor; + @After + public void after() throws Exception { + super.after(); + + ourRestServer.unregisterInterceptor(myRequestValidatingInterceptor); + } + @Before public void before() throws Exception { super.before(); - + myRequestValidatingInterceptor = new RequestValidatingInterceptor(); FhirInstanceValidator module = new FhirInstanceValidator(); module.setValidationSupport(myValidationSupport); myRequestValidatingInterceptor.addValidatorModule(module); } - @After - public void after() throws Exception { - super.after(); - - ourRestServer.unregisterInterceptor(myRequestValidatingInterceptor); - } - @Test public void testMultithreadedSearch() throws Exception { Bundle input = new Bundle(); @@ -56,8 +59,8 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { input.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); } ourClient.transaction().withBundle(input).execute(); - - + + List tasks = Lists.newArrayList(); try { for (int threadIndex = 0; threadIndex < 10; threadIndex++) { @@ -74,10 +77,9 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { validateNoErrors(tasks); } - - + /** - * This test prevents a deadlock that was detected with a large number of + * This test prevents a deadlock that was detected with a large number of * threads creating resources and blocking on the searchparamcache refreshing * (since this is a synchronized method) while the instance that was actually * executing was waiting on a DB connection. This was solved by making @@ -87,7 +89,7 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { @Test public void testMultithreadedSearchWithValidation() throws Exception { ourRestServer.registerInterceptor(myRequestValidatingInterceptor); - + Bundle input = new Bundle(); input.setType(BundleType.TRANSACTION); for (int i = 0; i < 500; i++) { @@ -96,7 +98,7 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { input.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST).setUrl("Patient"); } ourClient.transaction().withBundle(input).execute(); - + CloseableHttpResponse getMeta = ourHttpClient.execute(new HttpGet(ourServerBase + "/metadata")); try { assertEquals(200, getMeta.getStatusLine().getStatusCode()); @@ -133,7 +135,7 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { } total += next.getTaskCount(); } - + ourLog.info("Loaded {} searches", total); } @@ -142,14 +144,14 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { TestUtil.clearAllStaticFieldsForUnitTest(); } - public class BaseTask extends Thread { + public class BaseTask extends Thread { protected Throwable myError; protected int myTaskCount = 0; - + public BaseTask() { setDaemon(true); } - + public Throwable getError() { return myError; } @@ -168,7 +170,7 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { for (int i = 0; i < 10; i++) { try { Bundle respBundle; - + // Load search HttpGet get = new HttpGet(ourServerBase + "/Patient?identifier=http%3A%2F%2Ftest%7CBAR," + UUID.randomUUID().toString()); get.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW); @@ -181,7 +183,7 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { } finally { IOUtils.closeQuietly(getResp); } - + // Load page 2 get = new HttpGet(respBundle.getLink("next").getUrl()); get.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW); @@ -194,7 +196,7 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { } finally { IOUtils.closeQuietly(getResp); } - + } catch (Throwable e) { ourLog.error("Failure during search", e); myError = e; @@ -214,9 +216,9 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { p.addIdentifier().setSystem("http://test").setValue("BAR").setType(new CodeableConcept().addCoding(new Coding().setSystem("http://foo").setCode("bar"))); p.setGender(org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender.MALE); ourClient.create().resource(p).execute(); - + ourSearchParamRegistry.forceRefresh(); - + } catch (Throwable e) { ourLog.error("Failure during search", e); myError = e; @@ -226,4 +228,5 @@ public class StressTestDstu3Test extends BaseResourceProviderDstu3Test { } } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java index 007872d6992..b1764a79a90 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/EmailSubscriptionDstu2Test.java @@ -71,6 +71,7 @@ public class EmailSubscriptionDstu2Test extends BaseResourceProviderDstu2Test { mySubscriber.setEmailSender(emailSender); mySubscriber.setResourceDaos(myResourceDaos); mySubscriber.setFhirContext(myFhirCtx); + mySubscriber.setTxManager(ourTxManager); mySubscriber.start(); ourRestServer.registerInterceptor(mySubscriber); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java index 515e85d82e4..78505b03617 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/subscription/r4/RestHookTestR4Test.java @@ -51,7 +51,9 @@ public class RestHookTestR4Test extends BaseResourceProviderR4Test { @After public void afterUnregisterRestHookListener() { for (IIdType next : mySubscriptionIds) { - ourClient.delete().resourceById(next).execute(); + IIdType nextId = next.toUnqualifiedVersionless(); + ourLog.info("Deleting: {}", nextId); + ourClient.delete().resourceById(nextId).execute(); } mySubscriptionIds.clear(); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt b/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt index 21e69afe88a..1e8c7df6044 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt +++ b/hapi-fhir-jpaserver-uhnfhirtest/derby_maintenance.txt @@ -74,4 +74,24 @@ drop table trm_concept cascade constraints; drop table trm_concept_pc_link cascade constraints; drop table trm_concept_property cascade constraints; +# Delete all resources +update hfj_res_ver set forced_id_pid = null where res_id in (select res_id from hfj_resource); +update hfj_resource set forced_id_pid = null where res_id in (select res_id from hfj_resource); +delete from hfj_history_tag where res_id in (select res_id from hfj_resource); +delete from hfj_res_ver where res_id in (select res_id from hfj_resource); +delete from hfj_forced_id where resource_pid in (select res_id from hfj_resource); +delete from hfj_res_link where src_resource_id in (select res_id from hfj_resource); +delete from hfj_res_link where target_resource_id in (select res_id from hfj_resource); +delete from hfj_spidx_coords where res_id in (select res_id from hfj_resource); +delete from hfj_spidx_date where res_id in (select res_id from hfj_resource); +delete from hfj_spidx_number where res_id in (select res_id from hfj_resource); +delete from hfj_spidx_quantity where res_id in (select res_id from hfj_resource); +delete from hfj_spidx_string where res_id in (select res_id from hfj_resource); +delete from hfj_spidx_token where res_id in (select res_id from hfj_resource); +delete from hfj_spidx_uri where res_id in (select res_id from hfj_resource); +delete from hfj_res_tag where res_id in (select res_id from hfj_resource); +delete from hfj_search_result where resource_pid in (select res_id from hfj_resource); +delete from hfj_res_param_present where res_id in (select res_id from hfj_resource); +delete from hfj_resource where res_id in (select res_id from hfj_resource); + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 655a189274a..fe34b9350ba 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -42,6 +42,13 @@ JPA Page]]> for more information. + + In certain cases in the JPA server, if multiple threads all attempted to + update the same resource simultaneously, the optimistic lock failure caused + a "gap" in the history numbers to occur. This would then cause a mysterious + failure when trying to update this resource further. This has been + resolved. +