pointcut exceptions (#4822)

* enforce no exceptions on the invoking thread in PointCutLatch

* comment

* Msg.code

* Catch exceptions outside of await block

* fix migration issue

* Refactor PointcutLatch and add tests

* fix tests

* fix tests

* fix tests

* fix test

* fix test

* fix test

* fix test

* fix test

* fix test

* fix test

* fix test

* fix test

* fix test

* fix test!

* fix test!

* fix test

* fix test

* clean up latch error formatting

* fix test

* fix test

* fix test

* fix test

* fix test

* fix tests

* fix test

* fix test

* Remove hapi-fhir-jpaserver-uhnfhirtest from build as it is crashing CI

* fix test

* fix intermittent

* A SNAPSHOT dependency on an upstream module was added to HAPI FHIR, which changed and caused some downstream tests to break.

* fix intermittent

* Add Maven enforcer plugin for CR dependencies

* Make maven enforcer conditional on CI builds

* Remove hapi-fhir-jpaserver-uhnfhirtest from build as it is crashing CI

* improve test logging

* pre-review cleanup

* review feedback

* remove hapi-fhir-base-test-jaxrsserver-kotlin from the build

---------

Co-authored-by: Ken Stevens <ken@smilecdr.com>
Co-authored-by: Jonathan Percival <jonathan.i.percival@gmail.com>
This commit is contained in:
Ken Stevens 2023-05-11 22:31:38 -04:00 committed by GitHub
parent 0475cb682f
commit 4313dc9958
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 631 additions and 276 deletions

View File

@ -129,8 +129,8 @@ stages:
module: hapi-tinder-plugin module: hapi-tinder-plugin
- name: hapi_tinder_test - name: hapi_tinder_test
module: hapi-tinder-test module: hapi-tinder-test
- name: tests_hapi_fhir_base_test_jaxrsserver_kotlin # - name: tests_hapi_fhir_base_test_jaxrsserver_kotlin
module: tests/hapi-fhir-base-test-jaxrsserver-kotlin # module: tests/hapi-fhir-base-test-jaxrsserver-kotlin
- name: tests_hapi_fhir_base_test_mindeps_client - name: tests_hapi_fhir_base_test_mindeps_client
module: tests/hapi-fhir-base-test-mindeps-client module: tests/hapi-fhir-base-test-mindeps-client
- name: tests_hapi_fhir_base_test_mindeps_server - name: tests_hapi_fhir_base_test_mindeps_server

View File

@ -23,8 +23,6 @@ import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.Collection; import java.util.Collection;
@ -136,8 +134,14 @@ public class HookParams {
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this, ToStringStyle.SIMPLE_STYLE) StringBuilder b = new StringBuilder();
.append("params", myParams) myParams.forEach((key, value) -> {
.toString(); b.append(" ")
.append(key.getSimpleName())
.append(": ")
.append(value)
.append("\n");
});
return b.toString();
} }
} }

View File

@ -2,8 +2,8 @@ package ca.uhn.fhir.cli;
import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor;
import ca.uhn.fhir.test.utilities.TlsAuthenticationTestHelper;
import ca.uhn.fhir.test.utilities.RestServerR4Helper; import ca.uhn.fhir.test.utilities.RestServerR4Helper;
import ca.uhn.fhir.test.utilities.TlsAuthenticationTestHelper;
import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;
@ -29,7 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class ExampleDataUploaderTest { class ExampleDataUploaderTest {
@RegisterExtension @RegisterExtension
public final RestServerR4Helper myRestServerR4Helper = new RestServerR4Helper(); public final RestServerR4Helper myRestServerR4Helper = RestServerR4Helper.newWithTransactionLatch();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();
@ -46,7 +46,8 @@ class ExampleDataUploaderTest {
@ParameterizedTest @ParameterizedTest
@ValueSource(booleans = {true, false}) @ValueSource(booleans = {true, false})
public void testHeaderPassthrough(boolean theIncludeTls) throws ParseException { public void testHeaderPassthrough(boolean theIncludeTls) throws ParseException, InterruptedException {
// setup
String headerKey = "test-header-key"; String headerKey = "test-header-key";
String headerValue = "test header value"; String headerValue = "test header value";
@ -60,8 +61,11 @@ class ExampleDataUploaderTest {
); );
final CommandLine commandLine = new DefaultParser().parse(testedCommand.getOptions(), args, true); final CommandLine commandLine = new DefaultParser().parse(testedCommand.getOptions(), args, true);
testedCommand.run(commandLine);
// execute
myRestServerR4Helper.executeWithLatch(() -> runCommand(commandLine));
// validate
assertNotNull(myCapturingInterceptor.getLastRequest()); assertNotNull(myCapturingInterceptor.getLastRequest());
Map<String, List<String>> allHeaders = myCapturingInterceptor.getLastRequest().getAllHeaders(); Map<String, List<String>> allHeaders = myCapturingInterceptor.getLastRequest().getAllHeaders();
assertFalse(allHeaders.isEmpty()); assertFalse(allHeaders.isEmpty());
@ -78,6 +82,14 @@ class ExampleDataUploaderTest {
assertEquals("EX3152", resource.getIdElement().getIdPart()); assertEquals("EX3152", resource.getIdElement().getIdPart());
} }
private void runCommand(CommandLine commandLine) {
try {
testedCommand.run(commandLine);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
private static class RequestCapturingExampleDataUploader extends ExampleDataUploader { private static class RequestCapturingExampleDataUploader extends ExampleDataUploader {
private final CapturingInterceptor myCapturingInterceptor; private final CapturingInterceptor myCapturingInterceptor;

View File

@ -43,7 +43,7 @@ public class ExportConceptMapToCsvCommandDstu3Test {
} }
@RegisterExtension @RegisterExtension
public final RestServerDstu3Helper myRestServerDstu3Helper = new RestServerDstu3Helper(true); public final RestServerDstu3Helper myRestServerDstu3Helper = RestServerDstu3Helper.newInitialized();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();

View File

@ -40,7 +40,7 @@ public class ExportConceptMapToCsvCommandR4Test {
} }
@RegisterExtension @RegisterExtension
public final RestServerR4Helper myRestServerR4Helper = new RestServerR4Helper(true); public final RestServerR4Helper myRestServerR4Helper = RestServerR4Helper.newInitialized();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();

View File

@ -45,7 +45,7 @@ public class ImportCsvToConceptMapCommandDstu3Test {
} }
@RegisterExtension @RegisterExtension
public final RestServerDstu3Helper myRestServerDstu3Helper = new RestServerDstu3Helper(true); public final RestServerDstu3Helper myRestServerDstu3Helper = RestServerDstu3Helper.newInitialized();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();

View File

@ -50,7 +50,7 @@ public class ImportCsvToConceptMapCommandR4Test {
@RegisterExtension @RegisterExtension
public final RestServerR4Helper myRestServerR4Helper = new RestServerR4Helper(true); public final RestServerR4Helper myRestServerR4Helper = RestServerR4Helper.newInitialized();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();

View File

@ -39,7 +39,7 @@ class ReindexTerminologyCommandTest {
private BaseJpaSystemProvider<?, ?> myProvider = spy(new BaseJpaSystemProvider<>() {}); private BaseJpaSystemProvider<?, ?> myProvider = spy(new BaseJpaSystemProvider<>() {});
@RegisterExtension @RegisterExtension
public final RestServerR4Helper myRestServerR4Helper = new RestServerR4Helper(true); public final RestServerR4Helper myRestServerR4Helper = RestServerR4Helper.newInitialized();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();

View File

@ -104,9 +104,9 @@ public class UploadTerminologyCommandTest {
} }
@RegisterExtension @RegisterExtension
public final RestServerR4Helper myRestServerR4Helper = new RestServerR4Helper(true); public final RestServerR4Helper myRestServerR4Helper = RestServerR4Helper.newInitialized();
@RegisterExtension @RegisterExtension
public final RestServerDstu3Helper myRestServerDstu3Helper = new RestServerDstu3Helper(true); public final RestServerDstu3Helper myRestServerDstu3Helper = RestServerDstu3Helper.newInitialized();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();

View File

@ -166,7 +166,6 @@ import java.util.stream.Collectors;
import static java.util.Objects.isNull; import static java.util.Objects.isNull;
import static java.util.Objects.nonNull; import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.left; import static org.apache.commons.lang3.StringUtils.left;

View File

@ -7,7 +7,6 @@ import org.hl7.fhir.instance.model.api.IBaseBooleanDatatype;
import org.hl7.fhir.instance.model.api.IBaseCoding; import org.hl7.fhir.instance.model.api.IBaseCoding;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;

View File

@ -20,6 +20,7 @@ import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.function.Supplier;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -108,4 +109,19 @@ public abstract class BaseMdmHelper implements BeforeEachCallback, AfterEachCall
public PointcutLatch getAfterMdmLatch() { public PointcutLatch getAfterMdmLatch() {
return myAfterMdmLatch; return myAfterMdmLatch;
} }
/**
* Expect 1 call to the MDM_AFTER_PERSISTED_RESOURCE_CHECKED pointcut when calling theSupplier. Wait until
* the mdm message arrives and this pointcut is called before returning the result of theSupplier.
* @param theSupplier
* @return
* @param <T>
* @throws InterruptedException
*/
public <T> T executeWithLatch(Supplier<T> theSupplier) throws InterruptedException {
myAfterMdmLatch.setExpectedCount(1);
T retval = theSupplier.get();
myAfterMdmLatch.awaitExpected();
return retval;
}
} }

View File

@ -1,15 +1,20 @@
package ca.uhn.fhir.jpa.mdm.helper; package ca.uhn.fhir.jpa.mdm.helper;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
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.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.mdm.api.MdmLinkEvent;
import ca.uhn.fhir.rest.server.TransactionLogMessages; import ca.uhn.fhir.rest.server.TransactionLogMessages;
import ca.uhn.fhir.rest.server.messaging.ResourceOperationMessage;
import ca.uhn.test.concurrency.PointcutLatch;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import java.util.List;
import static ca.uhn.fhir.mdm.api.MdmConstants.CODE_GOLDEN_RECORD; import static ca.uhn.fhir.mdm.api.MdmConstants.CODE_GOLDEN_RECORD;
import static ca.uhn.fhir.mdm.api.MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS; import static ca.uhn.fhir.mdm.api.MdmConstants.SYSTEM_GOLDEN_RECORD_STATUS;
@ -27,8 +32,8 @@ public class MdmHelperR4 extends BaseMdmHelper {
public OutcomeAndLogMessageWrapper createWithLatch(IBaseResource theBaseResource, boolean isExternalHttpRequest) throws InterruptedException { public OutcomeAndLogMessageWrapper createWithLatch(IBaseResource theBaseResource, boolean isExternalHttpRequest) throws InterruptedException {
myAfterMdmLatch.setExpectedCount(1); myAfterMdmLatch.setExpectedCount(1);
DaoMethodOutcome daoMethodOutcome = doCreateResource(theBaseResource, isExternalHttpRequest); DaoMethodOutcome daoMethodOutcome = doCreateResource(theBaseResource, isExternalHttpRequest);
myAfterMdmLatch.awaitExpected(); List<HookParams> hookParams = myAfterMdmLatch.awaitExpected();
return new OutcomeAndLogMessageWrapper(daoMethodOutcome, myAfterMdmLatch.getLatchInvocationParameterOfType(TransactionLogMessages.class)); return new OutcomeAndLogMessageWrapper(daoMethodOutcome, hookParams);
} }
public OutcomeAndLogMessageWrapper updateWithLatch(IBaseResource theIBaseResource) throws InterruptedException { public OutcomeAndLogMessageWrapper updateWithLatch(IBaseResource theIBaseResource) throws InterruptedException {
@ -38,8 +43,8 @@ public class MdmHelperR4 extends BaseMdmHelper {
public OutcomeAndLogMessageWrapper updateWithLatch(IBaseResource theIBaseResource, boolean isExternalHttpRequest) throws InterruptedException { public OutcomeAndLogMessageWrapper updateWithLatch(IBaseResource theIBaseResource, boolean isExternalHttpRequest) throws InterruptedException {
myAfterMdmLatch.setExpectedCount(1); myAfterMdmLatch.setExpectedCount(1);
DaoMethodOutcome daoMethodOutcome = doUpdateResource(theIBaseResource, isExternalHttpRequest); DaoMethodOutcome daoMethodOutcome = doUpdateResource(theIBaseResource, isExternalHttpRequest);
myAfterMdmLatch.awaitExpected(); List<HookParams> hookParams = myAfterMdmLatch.awaitExpected();
return new OutcomeAndLogMessageWrapper(daoMethodOutcome, myAfterMdmLatch.getLatchInvocationParameterOfType(TransactionLogMessages.class)); return new OutcomeAndLogMessageWrapper(daoMethodOutcome, hookParams);
} }
public DaoMethodOutcome doCreateResource(IBaseResource theResource, boolean isExternalHttpRequest) { public DaoMethodOutcome doCreateResource(IBaseResource theResource, boolean isExternalHttpRequest) {
@ -68,12 +73,12 @@ public class MdmHelperR4 extends BaseMdmHelper {
* by the MDM module. * by the MDM module.
*/ */
public class OutcomeAndLogMessageWrapper { public class OutcomeAndLogMessageWrapper {
DaoMethodOutcome myDaoMethodOutcome; private final DaoMethodOutcome myDaoMethodOutcome;
TransactionLogMessages myLogMessages; private final List<HookParams> myHookParams;
private OutcomeAndLogMessageWrapper(DaoMethodOutcome theDaoMethodOutcome, TransactionLogMessages theTransactionLogMessages) { public OutcomeAndLogMessageWrapper(DaoMethodOutcome theDaoMethodOutcome, List<HookParams> theHookParams) {
myDaoMethodOutcome = theDaoMethodOutcome; myDaoMethodOutcome = theDaoMethodOutcome;
myLogMessages = theTransactionLogMessages; myHookParams = theHookParams;
} }
public DaoMethodOutcome getDaoMethodOutcome() { public DaoMethodOutcome getDaoMethodOutcome() {
@ -81,7 +86,19 @@ public class MdmHelperR4 extends BaseMdmHelper {
} }
public TransactionLogMessages getLogMessages() { public TransactionLogMessages getLogMessages() {
return myLogMessages; return PointcutLatch.getInvocationParameterOfType(myHookParams, TransactionLogMessages.class);
}
public List<HookParams> getHookParams() {
return myHookParams;
}
public MdmLinkEvent getMdmLinkEvent() {
return PointcutLatch.getInvocationParameterOfType(myHookParams, MdmLinkEvent.class);
}
public ResourceOperationMessage getResourceOperationMessage() {
return PointcutLatch.getInvocationParameterOfType(myHookParams, ResourceOperationMessage.class);
} }
} }

View File

@ -58,9 +58,9 @@ public class MdmEventIT extends BaseMdmR4Test {
addExternalEID(patient2, "eid-11"); addExternalEID(patient2, "eid-11");
addExternalEID(patient2, "eid-22"); addExternalEID(patient2, "eid-22");
myMdmHelper.updateWithLatch(patient2); MdmHelperR4.OutcomeAndLogMessageWrapper outcome = myMdmHelper.updateWithLatch(patient2);
MdmLinkEvent linkChangeEvent = myMdmHelper.getAfterMdmLatch().getLatchInvocationParameterOfType(MdmLinkEvent.class); MdmLinkEvent linkChangeEvent = outcome.getMdmLinkEvent();
assertNotNull(linkChangeEvent); assertNotNull(linkChangeEvent);
ourLog.info("Got event: {}", linkChangeEvent); ourLog.info("Got event: {}", linkChangeEvent);
@ -84,15 +84,15 @@ public class MdmEventIT extends BaseMdmR4Test {
@Test @Test
public void testCreateLinkChangeEvent() throws InterruptedException { public void testCreateLinkChangeEvent() throws InterruptedException {
Practitioner pr = buildPractitionerWithNameAndId("Young", "AC-DC"); Practitioner pr = buildPractitionerWithNameAndId("Young", "AC-DC");
myMdmHelper.createWithLatch(pr); MdmHelperR4.OutcomeAndLogMessageWrapper outcome = myMdmHelper.createWithLatch(pr);
ResourceOperationMessage resourceOperationMessage = myMdmHelper.getAfterMdmLatch().getLatchInvocationParameterOfType(ResourceOperationMessage.class); ResourceOperationMessage resourceOperationMessage = outcome.getResourceOperationMessage();
assertNotNull(resourceOperationMessage); assertNotNull(resourceOperationMessage);
assertEquals(pr.getIdElement().toUnqualifiedVersionless().getValue(), resourceOperationMessage.getId()); assertEquals(pr.getIdElement().toUnqualifiedVersionless().getValue(), resourceOperationMessage.getId());
MdmLink link = getLinkByTargetId(pr); MdmLink link = getLinkByTargetId(pr);
MdmLinkEvent linkChangeEvent = myMdmHelper.getAfterMdmLatch().getLatchInvocationParameterOfType(MdmLinkEvent.class); MdmLinkEvent linkChangeEvent = outcome.getMdmLinkEvent();
assertNotNull(linkChangeEvent); assertNotNull(linkChangeEvent);
assertEquals(1, linkChangeEvent.getMdmLinks().size()); assertEquals(1, linkChangeEvent.getMdmLinks().size());
@ -110,9 +110,9 @@ public class MdmEventIT extends BaseMdmR4Test {
@Test @Test
public void testUpdateLinkChangeEvent() throws InterruptedException { public void testUpdateLinkChangeEvent() throws InterruptedException {
Patient patient1 = addExternalEID(buildJanePatient(), "eid-1"); Patient patient1 = addExternalEID(buildJanePatient(), "eid-1");
myMdmHelper.createWithLatch(patient1); MdmHelperR4.OutcomeAndLogMessageWrapper outcome = myMdmHelper.createWithLatch(patient1);
MdmLinkEvent linkChangeEvent = myMdmHelper.getAfterMdmLatch().getLatchInvocationParameterOfType(MdmLinkEvent.class); MdmLinkEvent linkChangeEvent = outcome.getMdmLinkEvent();
assertNotNull(linkChangeEvent); assertNotNull(linkChangeEvent);
assertEquals(1, linkChangeEvent.getMdmLinks().size()); assertEquals(1, linkChangeEvent.getMdmLinks().size());

View File

@ -257,12 +257,11 @@ public class MdmSearchExpandingInterceptorIT extends BaseMdmR4Test {
} }
@Test @Test
public void testReferenceExpansionQuietlyFailsOnMissingMdmMatches() { public void testReferenceExpansionQuietlyFailsOnMissingMdmMatches() throws InterruptedException {
myStorageSettings.setAllowMdmExpansion(true); myStorageSettings.setAllowMdmExpansion(true);
Patient patient = buildJanePatient(); Patient patient = buildJanePatient();
patient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!"); patient.getMeta().addTag(MdmConstants.SYSTEM_MDM_MANAGED, MdmConstants.CODE_NO_MDM_MANAGED, "Don't MDM on me!");
DaoMethodOutcome daoMethodOutcome = myMdmHelper.doCreateResource(patient, true); String id = myMdmHelper.executeWithLatch(() -> myMdmHelper.doCreateResource(patient, true)).getId().getIdPart();
String id = daoMethodOutcome.getId().getIdPart();
createObservationWithSubject(id); createObservationWithSubject(id);
//Even though the user has NO mdm links, that should not cause a request failure. //Even though the user has NO mdm links, that should not cause a request failure.

View File

@ -32,7 +32,7 @@ public abstract class BaseProviderR4Test extends BaseMdmR4Test {
@Autowired @Autowired
private IMdmSubmitSvc myMdmSubmitSvc; private IMdmSubmitSvc myMdmSubmitSvc;
@Autowired @Autowired
private MdmSettings myMdmSettings; protected MdmSettings myMdmSettings;
@Autowired @Autowired
private MdmControllerHelper myMdmHelper; private MdmControllerHelper myMdmHelper;
@Autowired @Autowired

View File

@ -1,7 +1,7 @@
package ca.uhn.fhir.jpa.mdm.provider; package ca.uhn.fhir.jpa.mdm.provider;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.model.RequestPartitionId;
import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.jpa.entity.MdmLink;
import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.entity.PartitionEntity;
import ca.uhn.fhir.mdm.api.MdmConstants; import ca.uhn.fhir.mdm.api.MdmConstants;
@ -73,6 +73,7 @@ public class MdmProviderCreateLinkR4Test extends BaseLinkR4Test {
@Test @Test
public void testCreateLinkWithMatchResultOnDifferentPartitions() { public void testCreateLinkWithMatchResultOnDifferentPartitions() {
myPartitionSettings.setPartitioningEnabled(true); myPartitionSettings.setPartitioningEnabled(true);
myMdmSettings.setSearchAllPartitionForMatch(false);
myPartitionLookupSvc.createPartition(new PartitionEntity().setId(1).setName(PARTITION_1), null); myPartitionLookupSvc.createPartition(new PartitionEntity().setId(1).setName(PARTITION_1), null);
myPartitionLookupSvc.createPartition(new PartitionEntity().setId(2).setName(PARTITION_2), null); myPartitionLookupSvc.createPartition(new PartitionEntity().setId(2).setName(PARTITION_2), null);
assertLinkCount(1); assertLinkCount(1);

View File

@ -148,6 +148,7 @@ public class MdmProviderMergeGoldenResourcesR4Test extends BaseProviderR4Test {
@Test @Test
public void testMergeOnDifferentPartitions() { public void testMergeOnDifferentPartitions() {
myPartitionSettings.setPartitioningEnabled(true); myPartitionSettings.setPartitioningEnabled(true);
myMdmSettings.setSearchAllPartitionForMatch(false);
myPartitionLookupSvc.createPartition(new PartitionEntity().setId(1).setName(PARTITION_1), null); myPartitionLookupSvc.createPartition(new PartitionEntity().setId(1).setName(PARTITION_1), null);
RequestPartitionId requestPartitionId1 = RequestPartitionId.fromPartitionId(1); RequestPartitionId requestPartitionId1 = RequestPartitionId.fromPartitionId(1);
myPartitionLookupSvc.createPartition(new PartitionEntity().setId(2).setName(PARTITION_2), null); myPartitionLookupSvc.createPartition(new PartitionEntity().setId(2).setName(PARTITION_2), null);

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.jpa.subscription.channel.subscription; package ca.uhn.fhir.jpa.subscription.channel.subscription;
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -60,6 +61,7 @@ class SubscriptionChannelCache {
return myCache.containsKey(theChannelName); return myCache.containsKey(theChannelName);
} }
@VisibleForTesting
void logForUnitTest() { void logForUnitTest() {
for (String key : myCache.keySet()) { for (String key : myCache.keySet()) {
ourLog.info("SubscriptionChannelCache: {}", key); ourLog.info("SubscriptionChannelCache: {}", key);

View File

@ -28,6 +28,7 @@ import ca.uhn.fhir.jpa.subscription.channel.models.ReceivingChannelParameters;
import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription;
import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry;
import ca.uhn.fhir.jpa.subscription.model.ChannelRetryConfiguration; import ca.uhn.fhir.jpa.subscription.model.ChannelRetryConfiguration;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder; import com.google.common.collect.MultimapBuilder;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -147,4 +148,9 @@ public class SubscriptionChannelRegistry {
public synchronized int size() { public synchronized int size() {
return myDeliveryReceiverChannels.size(); return myDeliveryReceiverChannels.size();
} }
@VisibleForTesting
public void logForUnitTest() {
myDeliveryReceiverChannels.logForUnitTest();
}
} }

View File

@ -128,6 +128,8 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base
mySubscriptionMatchingPost.clear(); mySubscriptionMatchingPost.clear();
mySubscriptionActivatedPost.clear(); mySubscriptionActivatedPost.clear();
ourObservationListener.clear(); ourObservationListener.clear();
mySubscriptionResourceMatched.clear();
mySubscriptionResourceNotMatched.clear();
super.clearRegistry(); super.clearRegistry();
} }
@ -145,9 +147,11 @@ public abstract class BaseBlockingQueueSubscribableChannelDstu3Test extends Base
} }
protected Subscription sendSubscription(Subscription theSubscription, RequestPartitionId theRequestPartitionId, Boolean mockDao) throws InterruptedException { protected Subscription sendSubscription(Subscription theSubscription, RequestPartitionId theRequestPartitionId, Boolean mockDao) throws InterruptedException {
mySubscriptionResourceNotMatched.setExpectedCount(1);
mySubscriptionActivatedPost.setExpectedCount(1); mySubscriptionActivatedPost.setExpectedCount(1);
Subscription retVal = sendResource(theSubscription, theRequestPartitionId); Subscription retVal = sendResource(theSubscription, theRequestPartitionId);
mySubscriptionActivatedPost.awaitExpected(); mySubscriptionActivatedPost.awaitExpected();
mySubscriptionResourceNotMatched.awaitExpected();
return retVal; return retVal;
} }

View File

@ -35,7 +35,9 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri
assertEquals(2, mySubscriptionRegistry.size()); assertEquals(2, mySubscriptionRegistry.size());
ourObservationListener.setExpectedCount(1); ourObservationListener.setExpectedCount(1);
mySubscriptionResourceMatched.setExpectedCount(1);
sendObservation(code, "SNOMED-CT"); sendObservation(code, "SNOMED-CT");
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.awaitExpected(); ourObservationListener.awaitExpected();
assertEquals(1, ourContentTypes.size()); assertEquals(1, ourContentTypes.size());
@ -58,7 +60,9 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri
assertEquals(2, mySubscriptionRegistry.size()); assertEquals(2, mySubscriptionRegistry.size());
ourObservationListener.setExpectedCount(1); ourObservationListener.setExpectedCount(1);
mySubscriptionResourceMatched.setExpectedCount(1);
sendObservation(code, "SNOMED-CT"); sendObservation(code, "SNOMED-CT");
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.awaitExpected(); ourObservationListener.awaitExpected();
assertEquals(1, ourContentTypes.size()); assertEquals(1, ourContentTypes.size());
@ -82,7 +86,9 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri
mySubscriptionAfterDelivery.setExpectedCount(1); mySubscriptionAfterDelivery.setExpectedCount(1);
ourObservationListener.setExpectedCount(0); ourObservationListener.setExpectedCount(0);
mySubscriptionResourceMatched.setExpectedCount(1);
sendObservation(code, "SNOMED-CT"); sendObservation(code, "SNOMED-CT");
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.clear(); ourObservationListener.clear();
mySubscriptionAfterDelivery.awaitExpected(); mySubscriptionAfterDelivery.awaitExpected();
@ -120,7 +126,9 @@ public class SubscriptionCheckingSubscriberTest extends BaseBlockingQueueSubscri
observation.setStatus(Observation.ObservationStatus.FINAL); observation.setStatus(Observation.ObservationStatus.FINAL);
mySubscriptionResourceMatched.setExpectedCount(1);
sendResource(observation); sendResource(observation);
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.awaitExpected(); ourObservationListener.awaitExpected();
assertEquals(1, ourContentTypes.size()); assertEquals(1, ourContentTypes.size());

View File

@ -76,7 +76,9 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
assertEquals(2, mySubscriptionRegistry.size()); assertEquals(2, mySubscriptionRegistry.size());
ourObservationListener.setExpectedCount(1); ourObservationListener.setExpectedCount(1);
mySubscriptionResourceMatched.setExpectedCount(1);
sendObservation(code, "SNOMED-CT"); sendObservation(code, "SNOMED-CT");
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.awaitExpected(); ourObservationListener.awaitExpected();
assertEquals(1, ourContentTypes.size()); assertEquals(1, ourContentTypes.size());
@ -99,7 +101,9 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
assertEquals(2, mySubscriptionRegistry.size()); assertEquals(2, mySubscriptionRegistry.size());
ourObservationListener.setExpectedCount(1); ourObservationListener.setExpectedCount(1);
mySubscriptionResourceMatched.setExpectedCount(1);
sendObservation(code, "SNOMED-CT"); sendObservation(code, "SNOMED-CT");
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.awaitExpected(); ourObservationListener.awaitExpected();
assertEquals(1, ourContentTypes.size()); assertEquals(1, ourContentTypes.size());
@ -117,7 +121,9 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
assertEquals(1, mySubscriptionRegistry.size()); assertEquals(1, mySubscriptionRegistry.size());
ourObservationListener.setExpectedCount(1); ourObservationListener.setExpectedCount(1);
mySubscriptionResourceMatched.setExpectedCount(1);
sendResource(observation); sendResource(observation);
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.awaitExpected(); ourObservationListener.awaitExpected();
assertEquals(1, ourContentTypes.size()); assertEquals(1, ourContentTypes.size());
@ -141,7 +147,9 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
mySubscriptionAfterDelivery.setExpectedCount(1); mySubscriptionAfterDelivery.setExpectedCount(1);
ourObservationListener.setExpectedCount(0); ourObservationListener.setExpectedCount(0);
mySubscriptionResourceMatched.setExpectedCount(1);
sendObservation(code, "SNOMED-CT"); sendObservation(code, "SNOMED-CT");
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.clear(); ourObservationListener.clear();
mySubscriptionAfterDelivery.awaitExpected(); mySubscriptionAfterDelivery.awaitExpected();
@ -168,7 +176,9 @@ public class SubscriptionMatchingSubscriberTest extends BaseBlockingQueueSubscri
assertEquals(3, mySubscriptionRegistry.size()); assertEquals(3, mySubscriptionRegistry.size());
ourObservationListener.setExpectedCount(2); ourObservationListener.setExpectedCount(2);
mySubscriptionResourceMatched.setExpectedCount(2);
sendObservation(code, "SNOMED-CT"); sendObservation(code, "SNOMED-CT");
mySubscriptionResourceMatched.awaitExpected();
ourObservationListener.awaitExpected(); ourObservationListener.awaitExpected();
assertEquals(2, ourContentTypes.size()); assertEquals(2, ourContentTypes.size());

View File

@ -86,7 +86,10 @@ public class ExpungeHookTest extends BaseJpaDstu3Test {
options.setExpungeEverything(true); options.setExpungeEverything(true);
options.setExpungeDeletedResources(true); options.setExpungeDeletedResources(true);
options.setExpungeOldVersions(true); options.setExpungeOldVersions(true);
// TODO KHS shouldn't this be 1? Investigate why is it 2?
myExpungeResourceLatch.setExpectedCount(2);
myPatientDao.expunge(id.toUnqualifiedVersionless(), options, mySrd); myPatientDao.expunge(id.toUnqualifiedVersionless(), options, mySrd);
myExpungeResourceLatch.awaitExpected();
assertPatientGone(id); assertPatientGone(id);
// Create it a second time. // Create it a second time.

View File

@ -229,7 +229,9 @@ public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest()); assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
otherTestCallback.setInitExpectedCount(1);
otherCache.forceRefresh(); otherCache.forceRefresh();
otherTestCallback.awaitInitExpected();
assertEquals(2, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest()); assertEquals(2, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(myMaleTestCallback); myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(myMaleTestCallback);

View File

@ -42,24 +42,21 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
myStorageSettings.setEnforceReferentialIntegrityOnDelete(true); myStorageSettings.setEnforceReferentialIntegrityOnDelete(true);
} }
PointcutLatch myPointcutLatch = new PointcutLatch(Pointcut.STORAGE_TRANSACTION_PROCESSED); PointcutLatch myTransactionProcessedLatch = new PointcutLatch(Pointcut.STORAGE_TRANSACTION_PROCESSED);
@Autowired @Autowired
private IInterceptorService myInterceptorService; private IInterceptorService myInterceptorService;
@BeforeEach @BeforeEach
public void beforeEach() { public void beforeEach() {
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_TRANSACTION_PROCESSED, myPointcutLatch); myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_TRANSACTION_PROCESSED, myTransactionProcessedLatch);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, myPointcutLatch);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, myPointcutLatch);
myInterceptorService.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED, myPointcutLatch);
} }
@AfterEach @AfterEach
@Override @Override
public void afterResetInterceptors() { public void afterResetInterceptors() {
super.afterResetInterceptors(); super.afterResetInterceptors();
myInterceptorService.unregisterInterceptor(myPointcutLatch); myInterceptorService.unregisterInterceptor(myTransactionProcessedLatch);
} }
@Test @Test
@ -92,12 +89,9 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
//Delete an observation //Delete an observation
b.addEntry().getRequest().setMethod(DELETE).setUrl(daoMethodOutcome.getId().toUnqualifiedVersionless().getValue()); b.addEntry().getRequest().setMethod(DELETE).setUrl(daoMethodOutcome.getId().toUnqualifiedVersionless().getValue());
List<HookParams> hookParams = callTransaction(b);
myPointcutLatch.setExpectedCount(4); DeferredInterceptorBroadcasts broadcastsParam = hookParams.get(0).get(DeferredInterceptorBroadcasts.class);
mySystemDao.transaction(mySrd, b);
List<HookParams> hookParams = myPointcutLatch.awaitExpected();
DeferredInterceptorBroadcasts broadcastsParam = hookParams.get(3).get(DeferredInterceptorBroadcasts.class);
ListMultimap<Pointcut, HookParams> deferredInterceptorBroadcasts = broadcastsParam.getDeferredInterceptorBroadcasts(); ListMultimap<Pointcut, HookParams> deferredInterceptorBroadcasts = broadcastsParam.getDeferredInterceptorBroadcasts();
assertThat(deferredInterceptorBroadcasts.entries(), hasSize(3)); assertThat(deferredInterceptorBroadcasts.entries(), hasSize(3));
@ -118,7 +112,7 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
} }
@Test @Test
public void testDeleteInTransactionShouldSucceedWhenReferencesAreAlsoRemoved() { public void testDeleteInTransactionShouldSucceedWhenReferencesAreAlsoRemoved() throws InterruptedException {
final Observation obs1 = new Observation(); final Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL); obs1.setStatus(Observation.ObservationStatus.FINAL);
IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless(); IIdType obs1id = myObservationDao.create(obs1).getId().toUnqualifiedVersionless();
@ -141,7 +135,7 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
try { try {
// transaction should succeed because the DiagnosticReport which references obs2 is also deleted // transaction should succeed because the DiagnosticReport which references obs2 is also deleted
mySystemDao.transaction(mySrd, b); callTransaction(b);
} catch (ResourceVersionConflictException e) { } catch (ResourceVersionConflictException e) {
fail(); fail();
} }
@ -149,7 +143,7 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
@Test @Test
public void testDeleteWithHas_SourceModifiedToNoLongerIncludeReference() { public void testDeleteWithHas_SourceModifiedToNoLongerIncludeReference() throws InterruptedException {
Observation obs1 = new Observation(); Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL); obs1.setStatus(Observation.ObservationStatus.FINAL);
@ -173,7 +167,7 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
Bundle b = new Bundle(); Bundle b = new Bundle();
b.addEntry().getRequest().setMethod(DELETE).setUrl("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER"); b.addEntry().getRequest().setMethod(DELETE).setUrl("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER");
b.addEntry().setResource(rpt).getRequest().setMethod(PUT).setUrl("DiagnosticReport?identifier=foo|IDENTIFIER"); b.addEntry().setResource(rpt).getRequest().setMethod(PUT).setUrl("DiagnosticReport?identifier=foo|IDENTIFIER");
mySystemDao.transaction(mySrd, b); callTransaction(b);
myObservationDao.read(obs1id); myObservationDao.read(obs1id);
try { try {
@ -187,8 +181,14 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
assertThat(rpt.getResult(), empty()); assertThat(rpt.getResult(), empty());
} }
private List<HookParams> callTransaction(Bundle b) throws InterruptedException {
myTransactionProcessedLatch.setExpectedCount(1);
mySystemDao.transaction(mySrd, b);
return myTransactionProcessedLatch.awaitExpected();
}
@Test @Test
public void testDeleteWithId_SourceModifiedToNoLongerIncludeReference() { public void testDeleteWithId_SourceModifiedToNoLongerIncludeReference() throws InterruptedException {
Observation obs1 = new Observation(); Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL); obs1.setStatus(Observation.ObservationStatus.FINAL);
@ -211,7 +211,7 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
Bundle b = new Bundle(); Bundle b = new Bundle();
b.addEntry().getRequest().setMethod(DELETE).setUrl(obs1id.getValue()); b.addEntry().getRequest().setMethod(DELETE).setUrl(obs1id.getValue());
b.addEntry().setResource(rpt).getRequest().setMethod(PUT).setUrl(rptId.getValue()); b.addEntry().setResource(rpt).getRequest().setMethod(PUT).setUrl(rptId.getValue());
mySystemDao.transaction(mySrd, b); callTransaction(b);
myObservationDao.read(obs2id); myObservationDao.read(obs2id);
myDiagnosticReportDao.read(rptId); myDiagnosticReportDao.read(rptId);
@ -226,7 +226,7 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
@Test @Test
public void testDeleteWithHas_SourceModifiedToStillIncludeReference() { public void testDeleteWithHas_SourceModifiedToStillIncludeReference() throws InterruptedException {
Observation obs1 = new Observation(); Observation obs1 = new Observation();
obs1.setStatus(Observation.ObservationStatus.FINAL); obs1.setStatus(Observation.ObservationStatus.FINAL);
@ -252,7 +252,7 @@ public class TransactionHookTest extends BaseJpaR4SystemTest {
b.addEntry().getRequest().setMethod(DELETE).setUrl("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER"); b.addEntry().getRequest().setMethod(DELETE).setUrl("Observation?_has:DiagnosticReport:result:identifier=foo|IDENTIFIER");
b.addEntry().setResource(rpt).getRequest().setMethod(PUT).setUrl("DiagnosticReport?identifier=foo|IDENTIFIER"); b.addEntry().setResource(rpt).getRequest().setMethod(PUT).setUrl("DiagnosticReport?identifier=foo|IDENTIFIER");
try { try {
mySystemDao.transaction(mySrd, b); callTransaction(b);
fail(); fail();
} catch (ResourceVersionConflictException e ) { } catch (ResourceVersionConflictException e ) {
assertThat(e.getMessage(), matchesPattern(Msg.code(550) + Msg.code(515) + "Unable to delete Observation/[0-9]+ because at least one resource has a reference to this resource. First reference found was resource DiagnosticReport/[0-9]+ in path DiagnosticReport.result")); assertThat(e.getMessage(), matchesPattern(Msg.code(550) + Msg.code(515) + "Unable to delete Observation/[0-9]+ because at least one resource has a reference to this resource. First reference found was resource DiagnosticReport/[0-9]+ in path DiagnosticReport.result"));

View File

@ -19,13 +19,12 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.HumanName; import org.hl7.fhir.r4.model.HumanName;
import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Patient;
import javax.annotation.Nullable;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.PlatformTransactionManager;
import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@ -225,8 +224,8 @@ public class ThreadSafeResourceDeleterSvcTest extends BaseJpaR4Test {
public void cascadeDelete(RequestDetails theRequestDetails, DeleteConflictList theConflictList, IBaseResource theResource) throws InterruptedException { public void cascadeDelete(RequestDetails theRequestDetails, DeleteConflictList theConflictList, IBaseResource theResource) throws InterruptedException {
myCalledLatch.call(theResource); myCalledLatch.call(theResource);
ourLog.info("Waiting to proceed with delete"); ourLog.info("Waiting to proceed with delete");
myWaitLatch.awaitExpected(); List<HookParams> hookParams = myWaitLatch.awaitExpected();
ourLog.info("Cascade Delete proceeding: {}", myWaitLatch.getLatchInvocationParameter()); ourLog.info("Cascade Delete proceeding: {}", PointcutLatch.getLatchInvocationParameter(hookParams));
myWaitLatch.setExpectedCount(1); myWaitLatch.setExpectedCount(1);
} }

View File

@ -41,6 +41,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -214,10 +215,10 @@ public class BinaryAccessProviderR4Test extends BaseResourceProviderR4Test {
latch.setExpectedCount(1); latch.setExpectedCount(1);
try (CloseableHttpResponse resp = ourHttpClient.execute(get)) { try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
assertEquals(200, resp.getStatusLine().getStatusCode()); assertEquals(200, resp.getStatusLine().getStatusCode());
latch.awaitExpected(); List<HookParams> hookParams = latch.awaitExpected();
RequestDetails requestDetails = latch.getLatchInvocationParameterOfType(RequestDetails.class); RequestDetails requestDetails = PointcutLatch.getInvocationParameterOfType(hookParams, RequestDetails.class);
ResponseDetails responseDetails= latch.getLatchInvocationParameterOfType(ResponseDetails.class); ResponseDetails responseDetails= PointcutLatch.getInvocationParameterOfType(hookParams, ResponseDetails.class);
assertThat(responseDetails, is(notNullValue())); assertThat(responseDetails, is(notNullValue()));
assertThat(requestDetails, is(notNullValue())); assertThat(requestDetails, is(notNullValue()));

View File

@ -437,15 +437,15 @@ public abstract class BaseJpaR5Test extends BaseJpaTest implements ITestDataBuil
@Override @Override
protected void afterResetInterceptors() { protected void afterResetInterceptors() {
super.afterResetInterceptors(); super.afterResetInterceptors();
myInterceptorRegistry.unregisterInterceptor(myPerformanceTracingLoggingInterceptor); // myInterceptorRegistry.unregisterInterceptor(myPerformanceTracingLoggingInterceptor);
} }
@BeforeEach @BeforeEach
public void beforeCreateInterceptor() { public void beforeCreateInterceptor() {
myInterceptor = mock(IServerInterceptor.class); myInterceptor = mock(IServerInterceptor.class);
myPerformanceTracingLoggingInterceptor = new PerformanceTracingLoggingInterceptor(); // myPerformanceTracingLoggingInterceptor = new PerformanceTracingLoggingInterceptor();
myInterceptorRegistry.registerInterceptor(myPerformanceTracingLoggingInterceptor); // myInterceptorRegistry.registerInterceptor(myPerformanceTracingLoggingInterceptor);
} }
@BeforeEach @BeforeEach

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.subscription; package ca.uhn.fhir.jpa.subscription;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
@ -202,11 +203,15 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
ourCountHolder = myCountHolder; ourCountHolder = myCountHolder;
} }
// WIP STR5 consolidate with lambda
protected IIdType createResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException { protected IIdType createResource(IBaseResource theResource, boolean theExpectDelivery) throws InterruptedException {
return createResource(theResource, theExpectDelivery, 1);
}
// WIP STR5 consolidate with lambda
protected IIdType createResource(IBaseResource theResource, boolean theExpectDelivery, int theCount) throws InterruptedException {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
if (theExpectDelivery) { if (theExpectDelivery) {
mySubscriptionDeliveredLatch.setExpectedCount(1); mySubscriptionDeliveredLatch.setExpectedCount(theCount);
} }
mySubscriptionTopicsCheckedLatch.setExpectedCount(1); mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
IIdType id = dao.create(theResource, mySrd).getId(); IIdType id = dao.create(theResource, mySrd).getId();
@ -225,8 +230,8 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test
mySubscriptionTopicsCheckedLatch.setExpectedCount(1); mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
DaoMethodOutcome retval = dao.update(theResource, mySrd); DaoMethodOutcome retval = dao.update(theResource, mySrd);
mySubscriptionTopicsCheckedLatch.awaitExpected(); List<HookParams> hookParams = mySubscriptionTopicsCheckedLatch.awaitExpected();
ResourceModifiedMessage lastMessage = mySubscriptionTopicsCheckedLatch.getLatchInvocationParameterOfType(ResourceModifiedMessage.class); ResourceModifiedMessage lastMessage = PointcutLatch.getInvocationParameterOfType(hookParams, ResourceModifiedMessage.class);
assertEquals(theResource.getIdElement().toVersionless().toString(), lastMessage.getPayloadId()); assertEquals(theResource.getIdElement().toVersionless().toString(), lastMessage.getPayloadId());
if (theExpectDelivery) { if (theExpectDelivery) {

View File

@ -16,7 +16,6 @@ import org.slf4j.LoggerFactory;
import java.util.List; import java.util.List;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -30,6 +29,7 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
@Test @Test
public void testRestHookSubscriptionTopicApplicationFhirJson() throws Exception { public void testRestHookSubscriptionTopicApplicationFhirJson() throws Exception {
//setup
// WIP SR4B test update, delete, etc // WIP SR4B test update, delete, etc
createEncounterSubscriptionTopic(Enumerations.EncounterStatus.PLANNED, Enumerations.EncounterStatus.COMPLETED, SubscriptionTopic.InteractionTrigger.CREATE); createEncounterSubscriptionTopic(Enumerations.EncounterStatus.PLANNED, Enumerations.EncounterStatus.COMPLETED, SubscriptionTopic.InteractionTrigger.CREATE);
waitForRegisteredSubscriptionTopicCount(1); waitForRegisteredSubscriptionTopicCount(1);
@ -38,10 +38,11 @@ public class SubscriptionTopicR5Test extends BaseSubscriptionsR5Test {
waitForActivatedSubscriptionCount(1); waitForActivatedSubscriptionCount(1);
assertEquals(0, getSystemProviderCount()); assertEquals(0, getSystemProviderCount());
Encounter sentEncounter = sendEncounterWithStatus(Enumerations.EncounterStatus.COMPLETED, false);
await().until(() -> getSystemProviderCount() > 0); // execute
Encounter sentEncounter = sendEncounterWithStatus(Enumerations.EncounterStatus.COMPLETED, true);
// verify
Bundle receivedBundle = getLastSystemProviderBundle(); Bundle receivedBundle = getLastSystemProviderBundle();
List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle); List<IBaseResource> resources = BundleUtil.toListOfResources(myFhirCtx, receivedBundle);
assertEquals(2, resources.size()); assertEquals(2, resources.size());

View File

@ -77,6 +77,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
createObservationSubscriptionTopic(OBS_CODE2); createObservationSubscriptionTopic(OBS_CODE2);
waitForRegisteredSubscriptionTopicCount(2); waitForRegisteredSubscriptionTopicCount(2);
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW); Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW);
Subscription subscription = postSubscription(subscription1); Subscription subscription = postSubscription(subscription1);
@ -92,9 +93,14 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
assertEquals(sentObservation.getIdElement(), obs.getIdElement()); assertEquals(sentObservation.getIdElement(), obs.getIdElement());
} }
@NotNull
private Observation sendObservationExpectDelivery(int theCount) throws InterruptedException {
return sendObservation(OBS_CODE, "SNOMED-CT", true, theCount);
}
@NotNull @NotNull
private Observation sendObservationExpectDelivery() throws InterruptedException { private Observation sendObservationExpectDelivery() throws InterruptedException {
return sendObservation(true); return sendObservation(OBS_CODE, "SNOMED-CT", true, 1);
} }
@Test @Test
@ -274,6 +280,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
@NotNull @NotNull
private Subscription createTopicSubscription() throws InterruptedException { private Subscription createTopicSubscription() throws InterruptedException {
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_JSON_NEW); Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_JSON_NEW);
return postSubscription(subscription); return postSubscription(subscription);
@ -369,7 +376,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch); myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
ourLog.info("** About to send observation"); ourLog.info("** About to send observation");
Observation sentObservation = sendObservation(false); Observation sentObservation = sendObservation(OBS_CODE, "SNOMED-CT", false);
assertEquals("1", sentObservation.getIdElement().getVersionIdPart()); assertEquals("1", sentObservation.getIdElement().getVersionIdPart());
assertNull(sentObservation.getNoteFirstRep().getText()); assertNull(sentObservation.getNoteFirstRep().getText());
@ -418,7 +425,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch); myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(countDownLatch);
ourLog.info("** About to send observation"); ourLog.info("** About to send observation");
Observation sentObservation = sendObservation(false); Observation sentObservation = sendObservation(OBS_CODE, "SNOMED-CT", false);
assertEquals("1", sentObservation.getIdElement().getVersionIdPart()); assertEquals("1", sentObservation.getIdElement().getVersionIdPart());
assertNull(sentObservation.getNoteFirstRep().getText()); assertNull(sentObservation.getNoteFirstRep().getText());
@ -440,17 +447,20 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
@Test @Test
public void testRestHookSubscriptionApplicationJson() throws Exception { public void testRestHookSubscriptionApplicationJson() throws Exception {
ourLog.info(">>>1 Creating topics");
createObservationSubscriptionTopic(OBS_CODE); createObservationSubscriptionTopic(OBS_CODE);
createObservationSubscriptionTopic(OBS_CODE2); createObservationSubscriptionTopic(OBS_CODE2);
waitForRegisteredSubscriptionTopicCount(2); waitForRegisteredSubscriptionTopicCount(2);
// Subscribe to OBS_CODE topic ourLog.info(">>>2 Creating subscriptions");
Subscription subscription1 = createTopicSubscription(); Subscription subscription1 = createTopicSubscription();
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_JSON_NEW);
// Subscribe to OBS_CODE2 topic Subscription subscription2 = postSubscription(subscription);
Subscription subscription2 = postSubscription(newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_JSON_NEW));
waitForActivatedSubscriptionCount(2); waitForActivatedSubscriptionCount(2);
ourLog.info(">>>3 Send obs");
Observation sentObservation1 = sendObservationExpectDelivery(); Observation sentObservation1 = sendObservationExpectDelivery();
awaitUntilReceivedTransactionCount(1); awaitUntilReceivedTransactionCount(1);
Observation receivedObs = assertBundleAndGetObservation(subscription1, sentObservation1); Observation receivedObs = assertBundleAndGetObservation(subscription1, sentObservation1);
@ -462,25 +472,31 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
Subscription subscriptionTemp = myClient.read().resource(Subscription.class).withId(subscription2.getId()).execute(); Subscription subscriptionTemp = myClient.read().resource(Subscription.class).withId(subscription2.getId()).execute();
assertNotNull(subscriptionTemp); assertNotNull(subscriptionTemp);
subscriptionTemp.setTopic(subscription1.getTopic()); subscriptionTemp.setTopic(subscription1.getTopic());
ourLog.info(">>>4 Update sub");
updateResource(subscriptionTemp, false); updateResource(subscriptionTemp, false);
Observation observation2 = sendObservationExpectDelivery(); ourLog.info(">>>5 Send obs");
Observation observation2 = sendObservationExpectDelivery(2);
// Should see two subscription notifications since both now point to OBS_CODE2
awaitUntilReceivedTransactionCount(3); awaitUntilReceivedTransactionCount(3);
// Delete the second subscription ourLog.info(">>>6 Delete sub");
ourLog.info(">>> Deleting {}", subscription2.getId());
deleteSubscription(subscription2); deleteSubscription(subscription2);
waitForActivatedSubscriptionCount(1);
ourLog.info(">>>7 Send obs");
IdType observationTemp3Id = sendObservationExpectDelivery().getIdElement().toUnqualifiedVersionless(); IdType observationTemp3Id = sendObservationExpectDelivery().getIdElement().toUnqualifiedVersionless();
// Should see only one subscription notification // Should see only one subscription notification
awaitUntilReceivedTransactionCount(4); awaitUntilReceivedTransactionCount(4);
// Now update the observation to have OBS_CODE2
Observation observation3 = myClient.read().resource(Observation.class).withId(observationTemp3Id).execute(); Observation observation3 = myClient.read().resource(Observation.class).withId(observationTemp3Id).execute();
setCode(observation3, OBS_CODE2); CodeableConcept codeableConcept = new CodeableConcept();
observation3.setCode(codeableConcept);
Coding coding = codeableConcept.addCoding();
coding.setCode(OBS_CODE + "111");
coding.setSystem("SNOMED-CT");
ourLog.info(">>>8 Send obs");
updateResource(observation3, true); updateResource(observation3, true);
// Should see one subscription notification even though the new version doesn't match, the old version still does and our subscription topic // Should see one subscription notification even though the new version doesn't match, the old version still does and our subscription topic
@ -489,8 +505,12 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
Observation observation3a = myClient.read().resource(Observation.class).withId(observationTemp3Id).execute(); Observation observation3a = myClient.read().resource(Observation.class).withId(observationTemp3Id).execute();
// Now update it back to OBS_CODE again CodeableConcept codeableConcept1 = new CodeableConcept();
setCode(observation3a, OBS_CODE); observation3a.setCode(codeableConcept1);
Coding coding1 = codeableConcept1.addCoding();
coding1.setCode(OBS_CODE);
coding1.setSystem("SNOMED-CT");
ourLog.info(">>>9 Send obs");
updateResource(observation3a, true); updateResource(observation3a, true);
// Should see exactly one subscription notification // Should see exactly one subscription notification
@ -501,14 +521,6 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
assertFalse(observation2.getId().isEmpty()); assertFalse(observation2.getId().isEmpty());
} }
private static void setCode(Observation observation3a, String obsCode) {
CodeableConcept codeableConcept1 = new CodeableConcept();
observation3a.setCode(codeableConcept1);
Coding coding1 = codeableConcept1.addCoding();
coding1.setCode(obsCode);
coding1.setSystem("SNOMED-CT");
}
private void deleteSubscription(Subscription subscription2) throws InterruptedException { private void deleteSubscription(Subscription subscription2) throws InterruptedException {
mySubscriptionTopicsCheckedLatch.setExpectedCount(1); mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute(); myClient.delete().resourceById(new IdType("Subscription/" + subscription2.getId())).execute();
@ -516,19 +528,12 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
} }
private void awaitUntilReceivedTransactionCount(int theExpected) { private void awaitUntilReceivedTransactionCount(int theExpected) {
if (getSystemProviderCount() == theExpected) {
String list = getReceivedObservations().stream()
.map(t -> t.getIdElement().toUnqualifiedVersionless().getValue() + " " + t.getCode().getCodingFirstRep().getCode())
.collect(Collectors.joining(", "));
ourLog.info("Received {} transactions as expected: {}", theExpected, list);
} else {
String list = getReceivedObservations().stream() String list = getReceivedObservations().stream()
.map(t -> t.getIdElement().toUnqualifiedVersionless().getValue() + " " + t.getCode().getCodingFirstRep().getCode()) .map(t -> t.getIdElement().toUnqualifiedVersionless().getValue() + " " + t.getCode().getCodingFirstRep().getCode())
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
String errorMessage = "Expected " + theExpected + " transactions, have " + getSystemProviderCount() + ": " + list; String errorMessage = "Expected " + theExpected + " transactions, have " + getSystemProviderCount() + ": " + list;
await(errorMessage).until(() -> getSystemProviderCount() == theExpected); await(errorMessage).until(() -> getSystemProviderCount() == theExpected);
} }
}
@Test @Test
public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception { public void testRestHookSubscriptionApplicationJsonDatabase() throws Exception {
@ -539,6 +544,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
@Nonnull @Nonnull
private Subscription createTopicSubscription(String theTopicUrlSuffix) throws InterruptedException { private Subscription createTopicSubscription(String theTopicUrlSuffix) throws InterruptedException {
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + theTopicUrlSuffix, Constants.CT_FHIR_JSON_NEW); Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + theTopicUrlSuffix, Constants.CT_FHIR_JSON_NEW);
return postSubscription(subscription); return postSubscription(subscription);
@ -548,6 +554,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
public void testSubscriptionTriggerViaSubscription() throws Exception { public void testSubscriptionTriggerViaSubscription() throws Exception {
createSubscriptionTopic(); createSubscriptionTopic();
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW); Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW);
Subscription subscription = postSubscription(subscription1); Subscription subscription = postSubscription(subscription1);
@ -601,6 +608,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
ourLog.info("** About to create non-matching subscription"); ourLog.info("** About to create non-matching subscription");
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW); Subscription subscription1 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW);
Subscription subscription = postSubscription(subscription1); Subscription subscription = postSubscription(subscription1);
@ -608,11 +616,11 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
ourLog.info("** About to send observation that wont match"); ourLog.info("** About to send observation that wont match");
sendObservation(false); sendObservation(OBS_CODE, "SNOMED-CT", false);
awaitUntilReceivedTransactionCount(0); awaitUntilReceivedTransactionCount(0);
ourLog.info("** About to update subscription topic"); ourLog.info("** About to update subscription topic");
SubscriptionTopic subscriptionTopicTemp = myClient.read().resource(SubscriptionTopic.class).withId(subscriptionTopic.getId()).execute(); SubscriptionTopic subscriptionTopicTemp = myClient.read(SubscriptionTopic.class, subscriptionTopic.getId());
assertNotNull(subscriptionTopicTemp); assertNotNull(subscriptionTopicTemp);
setSubscriptionTopicCriteria(subscriptionTopicTemp, "Observation?code=SNOMED-CT|" + OBS_CODE); setSubscriptionTopicCriteria(subscriptionTopicTemp, "Observation?code=SNOMED-CT|" + OBS_CODE);
updateResource(subscriptionTopicTemp, false); updateResource(subscriptionTopicTemp, false);
@ -625,7 +633,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
deleteSubscription(subscription); deleteSubscription(subscription);
sendObservation(false); sendObservation(OBS_CODE, "SNOMED-CT", false);
// No more matches // No more matches
awaitUntilReceivedTransactionCount(1); awaitUntilReceivedTransactionCount(1);
@ -641,9 +649,11 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
createObservationSubscriptionTopic(OBS_CODE2); createObservationSubscriptionTopic(OBS_CODE2);
waitForRegisteredSubscriptionTopicCount(2); waitForRegisteredSubscriptionTopicCount(2);
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription3 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW); Subscription subscription3 = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE, Constants.CT_FHIR_XML_NEW);
postSubscription(subscription3); postSubscription(subscription3);
// WIP STR5 will likely require matching TopicSubscription
Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW); Subscription subscription = newTopicSubscription(SUBSCRIPTION_TOPIC_TEST_URL + OBS_CODE2, Constants.CT_FHIR_XML_NEW);
postSubscription(subscription); postSubscription(subscription);
@ -675,10 +685,11 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
} }
} }
private void createSubscriptionTopicWithCriteria(String theCriteria) throws InterruptedException { @Nonnull
private SubscriptionTopic createSubscriptionTopicWithCriteria(String theCriteria) throws InterruptedException {
SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(CUSTOM_URL); SubscriptionTopic subscriptionTopic = buildSubscriptionTopic(CUSTOM_URL);
setSubscriptionTopicCriteria(subscriptionTopic, theCriteria); setSubscriptionTopicCriteria(subscriptionTopic, theCriteria);
createSubscriptionTopic(subscriptionTopic); return createSubscriptionTopic(subscriptionTopic);
} }
@Test @Test
@ -699,7 +710,7 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
updateResource(subscription, false); updateResource(subscription, false);
// Send another observation // Send another observation
sendObservation(false); sendObservation(OBS_CODE, "SNOMED-CT", false);
// Should see no new delivery // Should see no new delivery
awaitUntilReceivedTransactionCount(1); awaitUntilReceivedTransactionCount(1);
@ -780,7 +791,9 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
sp.setType(Enumerations.SearchParamType.TOKEN); sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setExpression("Observation.extension('Observation#accessType')"); sp.setExpression("Observation.extension('Observation#accessType')");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
mySubscriptionTopicsCheckedLatch.setExpectedCount(1);
mySearchParameterDao.create(sp, mySrd); mySearchParameterDao.create(sp, mySrd);
mySubscriptionTopicsCheckedLatch.awaitExpected();
mySearchParamRegistry.forceRefresh(); mySearchParamRegistry.forceRefresh();
createSubscriptionTopicWithCriteria(criteria); createSubscriptionTopicWithCriteria(criteria);
waitForRegisteredSubscriptionTopicCount(1); waitForRegisteredSubscriptionTopicCount(1);
@ -846,20 +859,22 @@ public class RestHookTestR5IT extends BaseSubscriptionsR5Test {
return retval; return retval;
} }
private Observation sendObservation(String theCode, String theSystem, boolean theExpectDelivery) throws InterruptedException {
return sendObservation(theCode, theSystem, theExpectDelivery, 1);
}
private Observation sendObservation(boolean theExpectDelivery) throws private Observation sendObservation(String theCode, String theSystem, boolean theExpectDelivery, int theCount) throws InterruptedException {
InterruptedException {
Observation observation = new Observation(); Observation observation = new Observation();
CodeableConcept codeableConcept = new CodeableConcept(); CodeableConcept codeableConcept = new CodeableConcept();
observation.setCode(codeableConcept); observation.setCode(codeableConcept);
observation.getIdentifierFirstRep().setSystem("foo").setValue("1"); observation.getIdentifierFirstRep().setSystem("foo").setValue("1");
Coding coding = codeableConcept.addCoding(); Coding coding = codeableConcept.addCoding();
coding.setCode(OBS_CODE); coding.setCode(theCode);
coding.setSystem("SNOMED-CT"); coding.setSystem(theSystem);
observation.setStatus(Enumerations.ObservationStatus.FINAL); observation.setStatus(Enumerations.ObservationStatus.FINAL);
IIdType id = createResource(observation, theExpectDelivery); IIdType id = createResource(observation, theExpectDelivery, theCount);
observation.setId(id); observation.setId(id);
return observation; return observation;

View File

@ -39,7 +39,7 @@ public class CountingInterceptor implements ChannelInterceptor {
@Override @Override
public void afterSendCompletion(Message<?> theMessage, MessageChannel theChannel, boolean theSent, Exception theException) { public void afterSendCompletion(Message<?> theMessage, MessageChannel theChannel, boolean theSent, Exception theException) {
ourLog.info("Counting another instance: {}", theMessage); ourLog.info("Send complete for message: {}", theMessage);
if (theSent) { if (theSent) {
mySent.add(theMessage.toString()); mySent.add(theMessage.toString());
} }

View File

@ -217,12 +217,12 @@ class HapiMigratorIT {
protected void doExecute() { protected void doExecute() {
try { try {
myLatch.call(this); myLatch.call(this);
myWaitLatch.awaitExpected(); List<HookParams> hookParams = myWaitLatch.awaitExpected();
ourLog.info("Latch released with parameter {}", myWaitLatch.getLatchInvocationParameter()); ourLog.info("Latch released with parameter {}", PointcutLatch.getLatchInvocationParameter(hookParams));
// We sleep a bit to ensure the other thread has a chance to try to get the lock. We don't have a hook there, so sleep instead // We sleep a bit to ensure the other thread has a chance to try to get the lock. We don't have a hook there, so sleep instead
// Maybe we can await on a log message? // Maybe we can await on a log message?
Thread.sleep(200); Thread.sleep(200);
ourLog.info("Completing execution of {}", myWaitLatch.getLatchInvocationParameter()); ourLog.info("Completing execution of {}", PointcutLatch.getLatchInvocationParameter(hookParams));
} catch (InterruptedException e) { } catch (InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View File

@ -26,19 +26,16 @@ import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc;
import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider; import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.parser.DataFormatException;
import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions; import ca.uhn.fhir.rest.api.server.bulk.BulkDataExportOptions;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;

View File

@ -29,7 +29,6 @@ import org.hl7.fhir.r4.model.Measure;
import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Parameters;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;

View File

@ -19,10 +19,10 @@
*/ */
package ca.uhn.fhir.cr.r4.measure; package ca.uhn.fhir.cr.r4.measure;
import ca.uhn.fhir.cr.enumeration.CareGapsStatusCode;
import ca.uhn.fhir.cr.common.IDaoRegistryUser; import ca.uhn.fhir.cr.common.IDaoRegistryUser;
import ca.uhn.fhir.cr.common.Searches; import ca.uhn.fhir.cr.common.Searches;
import ca.uhn.fhir.cr.config.CrProperties; import ca.uhn.fhir.cr.config.CrProperties;
import ca.uhn.fhir.cr.enumeration.CareGapsStatusCode;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -59,7 +59,6 @@ import org.opencds.cqf.cql.evaluator.fhir.util.Ids;
import org.opencds.cqf.cql.evaluator.fhir.util.Resources; import org.opencds.cqf.cql.evaluator.fhir.util.Resources;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.ArrayList; import java.util.ArrayList;

View File

@ -25,7 +25,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.IdType;
@ -339,13 +338,14 @@ public class CanonicalSubscription implements Serializable, Cloneable, IModelJso
@Override @Override
public String toString() { public String toString() {
ToStringBuilder stringBuilder = new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) ToStringBuilder stringBuilder = new ToStringBuilder(this)
.append("idElement", myIdElement); .append("myIdElement", myIdElement)
// .append("status", myStatus) .append("myStatus", myStatus)
// .append("endpointUrl", myEndpointUrl) .append("myCriteriaString", myCriteriaString);
// .append("payloadString", myPayloadString) // .append("myEndpointUrl", myEndpointUrl)
// .append("myPayloadString", myPayloadString)
// .append("myHeaders", myHeaders) // .append("myHeaders", myHeaders)
// .append("channelType", myChannelType); // .append("myChannelType", myChannelType)
// .append("myTrigger", myTrigger) // .append("myTrigger", myTrigger)
// .append("myEmailDetails", myEmailDetails) // .append("myEmailDetails", myEmailDetails)
// .append("myRestHookDetails", myRestHookDetails) // .append("myRestHookDetails", myRestHookDetails)

View File

@ -127,7 +127,7 @@ public class ResourceDeliveryMessage extends BaseResourceMessage implements IRes
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this) return new ToStringBuilder(this)
.append("mySubscription", mySubscription) .append("mySubscription", mySubscription == null ? "null" : mySubscription.getIdElementString())
// it isn't safe to log payloads // it isn't safe to log payloads
.append("myPayloadString", "[Not Logged]") .append("myPayloadString", "[Not Logged]")
.append("myPayload", myPayloadDecoded) .append("myPayload", myPayloadDecoded)

View File

@ -36,9 +36,9 @@ import java.util.stream.Stream;
public abstract class BaseFhirVersionParameterizedTest { public abstract class BaseFhirVersionParameterizedTest {
@RegisterExtension @RegisterExtension
public final RestServerR4Helper myRestServerR4Helper = new RestServerR4Helper(true); public final RestServerR4Helper myRestServerR4Helper = RestServerR4Helper.newInitialized();
@RegisterExtension @RegisterExtension
public final RestServerDstu3Helper myRestServerDstu3Helper = new RestServerDstu3Helper(true); public final RestServerDstu3Helper myRestServerDstu3Helper = RestServerDstu3Helper.newInitialized();
@RegisterExtension @RegisterExtension
public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper(); public TlsAuthenticationTestHelper myTlsAuthenticationTestHelper = new TlsAuthenticationTestHelper();

View File

@ -20,7 +20,6 @@
package ca.uhn.fhir.test.utilities; package ca.uhn.fhir.test.utilities;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.rest.annotation.Transaction; import ca.uhn.fhir.rest.annotation.Transaction;
@ -50,17 +49,18 @@ import javax.servlet.ServletException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.function.Supplier;
public class RestServerDstu3Helper extends BaseRestServerHelper implements IPointcutLatch, BeforeEachCallback, AfterEachCallback { public class RestServerDstu3Helper extends BaseRestServerHelper implements IPointcutLatch, BeforeEachCallback, AfterEachCallback {
protected final MyRestfulServer myRestServer; protected final MyRestfulServer myRestServer;
public RestServerDstu3Helper() { public RestServerDstu3Helper() {
this(false); this(false, false);
} }
public RestServerDstu3Helper(boolean theInitialize) { private RestServerDstu3Helper(boolean theInitialize, boolean theTransactionLatchEnabled) {
super(FhirContext.forDstu3()); super(FhirContext.forDstu3());
myRestServer = new MyRestfulServer(myFhirContext); myRestServer = new MyRestfulServer(myFhirContext, theTransactionLatchEnabled);
if (theInitialize) { if (theInitialize) {
try { try {
myRestServer.initialize(); myRestServer.initialize();
@ -70,6 +70,14 @@ public class RestServerDstu3Helper extends BaseRestServerHelper implements IPoin
} }
} }
public static RestServerDstu3Helper newInitialized() {
return new RestServerDstu3Helper(true, false);
}
public static RestServerDstu3Helper newWithTransactionLatch() {
return new RestServerDstu3Helper(false, true);
}
@Override @Override
public void beforeEach(ExtensionContext context) throws Exception { public void beforeEach(ExtensionContext context) throws Exception {
startServer(myRestServer); startServer(myRestServer);
@ -174,32 +182,56 @@ public class RestServerDstu3Helper extends BaseRestServerHelper implements IPoin
public static class MyPlainProvider implements IPointcutLatch { public static class MyPlainProvider implements IPointcutLatch {
private final PointcutLatch myPointcutLatch = new PointcutLatch("Transaction Counting Provider"); private final PointcutLatch myPointcutLatch = new PointcutLatch("Transaction Counting Provider");
private final List<IBaseBundle> myTransactions = Collections.synchronizedList(new ArrayList<>()); private final List<IBaseBundle> myTransactions = Collections.synchronizedList(new ArrayList<>());
private boolean myTransactionLatchEnabled;
public MyPlainProvider(boolean theTransactionLatchEnabled) {
this.myTransactionLatchEnabled = theTransactionLatchEnabled;
}
@Transaction @Transaction
public synchronized IBaseBundle transaction(@TransactionParam IBaseBundle theBundle) { public synchronized IBaseBundle transaction(@TransactionParam IBaseBundle theBundle) {
if (myTransactionLatchEnabled) {
myPointcutLatch.call(theBundle); myPointcutLatch.call(theBundle);
}
myTransactions.add(theBundle); myTransactions.add(theBundle);
return theBundle; return theBundle;
} }
@Override @Override
public void clear() { public void clear() {
if (!myTransactionLatchEnabled) {
throw new IllegalStateException("Can't call clear() on a provider that doesn't use a latch");
}
myPointcutLatch.clear(); myPointcutLatch.clear();
} }
@Override @Override
public void setExpectedCount(int theCount) { public void setExpectedCount(int theCount) {
if (!myTransactionLatchEnabled) {
throw new IllegalStateException("Can't call clear() on a provider that doesn't use a latch");
}
myPointcutLatch.setExpectedCount(theCount); myPointcutLatch.setExpectedCount(theCount);
} }
@Override @Override
public List<HookParams> awaitExpected() throws InterruptedException { public List<HookParams> awaitExpected() throws InterruptedException {
if (!myTransactionLatchEnabled) {
throw new IllegalStateException("Can't call clear() on a provider that doesn't use a latch");
}
return myPointcutLatch.awaitExpected(); return myPointcutLatch.awaitExpected();
} }
public List<IBaseBundle> getTransactions() { public List<IBaseBundle> getTransactions() {
return Collections.unmodifiableList(new ArrayList<>(myTransactions)); return Collections.unmodifiableList(new ArrayList<>(myTransactions));
} }
public void setTransactionLatchEnabled(boolean theTransactionLatchEnabled) {
this.myTransactionLatchEnabled = theTransactionLatchEnabled;
}
public boolean isTransactionLatchEnabled() {
return myTransactionLatchEnabled;
}
} }
private static class MyRestfulServer extends RestfulServer { private static class MyRestfulServer extends RestfulServer {
@ -209,15 +241,24 @@ public class RestServerDstu3Helper extends BaseRestServerHelper implements IPoin
private HashMapResourceProvider<Organization> myOrganizationResourceProvider; private HashMapResourceProvider<Organization> myOrganizationResourceProvider;
private HashMapResourceProvider<ConceptMap> myConceptMapResourceProvider; private HashMapResourceProvider<ConceptMap> myConceptMapResourceProvider;
private MyPlainProvider myPlainProvider; private MyPlainProvider myPlainProvider;
private final boolean myInitialTransactionLatchEnabled;
public MyRestfulServer(FhirContext theFhirContext) { public MyRestfulServer(FhirContext theFhirContext, boolean theInitialTransactionLatchEnabled) {
super(theFhirContext); super(theFhirContext);
myInitialTransactionLatchEnabled = theInitialTransactionLatchEnabled;
} }
public MyPlainProvider getPlainProvider() { public MyPlainProvider getPlainProvider() {
return myPlainProvider; return myPlainProvider;
} }
public <T> T executeWithLatch(Supplier<T> theSupplier) throws InterruptedException {
myPlainProvider.setExpectedCount(1);
T retval = theSupplier.get();
myPlainProvider.awaitExpected();
return retval;
}
public void setFailNextPut(boolean theFailNextPut) { public void setFailNextPut(boolean theFailNextPut) {
myFailNextPut = theFailNextPut; myFailNextPut = theFailNextPut;
} }
@ -229,7 +270,16 @@ public class RestServerDstu3Helper extends BaseRestServerHelper implements IPoin
provider.clearCounts(); provider.clearCounts();
} }
} }
myPlainProvider.clear(); if (isTransactionLatchEnabled()) {
getPlainProvider().clear();
}
}
private boolean isTransactionLatchEnabled() {
if (getPlainProvider() == null) {
return false;
}
return getPlainProvider().isTransactionLatchEnabled();
} }
public void clearDataAndCounts() { public void clearDataAndCounts() {
@ -272,7 +322,7 @@ public class RestServerDstu3Helper extends BaseRestServerHelper implements IPoin
myConceptMapResourceProvider = new MyHashMapResourceProvider(fhirContext, ConceptMap.class); myConceptMapResourceProvider = new MyHashMapResourceProvider(fhirContext, ConceptMap.class);
registerProvider(myConceptMapResourceProvider); registerProvider(myConceptMapResourceProvider);
myPlainProvider = new MyPlainProvider(); myPlainProvider = new MyPlainProvider(myInitialTransactionLatchEnabled);
registerProvider(myPlainProvider); registerProvider(myPlainProvider);
} }

View File

@ -61,12 +61,12 @@ public class RestServerR4Helper extends BaseRestServerHelper implements BeforeEa
protected final MyRestfulServer myRestServer; protected final MyRestfulServer myRestServer;
public RestServerR4Helper() { public RestServerR4Helper() {
this(false); this(false, false);
} }
public RestServerR4Helper(boolean theInitialize) { private RestServerR4Helper(boolean theInitialize, boolean theTransactionLatchEnabled) {
super(FhirContext.forR4Cached()); super(FhirContext.forR4Cached());
myRestServer = new MyRestfulServer(myFhirContext); myRestServer = new MyRestfulServer(myFhirContext, theTransactionLatchEnabled);
if (theInitialize) { if (theInitialize) {
try { try {
myRestServer.initialize(); myRestServer.initialize();
@ -76,6 +76,14 @@ public class RestServerR4Helper extends BaseRestServerHelper implements BeforeEa
} }
} }
public static RestServerR4Helper newWithTransactionLatch() {
return new RestServerR4Helper(false, true);
}
public static RestServerR4Helper newInitialized() {
return new RestServerR4Helper(true, false);
}
@Override @Override
public void beforeEach(ExtensionContext context) throws Exception { public void beforeEach(ExtensionContext context) throws Exception {
startServer(myRestServer); startServer(myRestServer);
@ -252,6 +260,14 @@ public class RestServerR4Helper extends BaseRestServerHelper implements BeforeEa
myRestServer.setServerAddressStrategy(theServerAddressStrategy); myRestServer.setServerAddressStrategy(theServerAddressStrategy);
} }
public void executeWithLatch(Runnable theRunnable) throws InterruptedException {
myRestServer.executeWithLatch(theRunnable);
}
public void enableTransactionLatch(boolean theTransactionLatchEnabled) {
myRestServer.setTransactionLatchEnabled(theTransactionLatchEnabled);
}
private static class MyRestfulServer extends RestfulServer { private static class MyRestfulServer extends RestfulServer {
private final List<String> myRequestUrls = Collections.synchronizedList(new ArrayList<>()); private final List<String> myRequestUrls = Collections.synchronizedList(new ArrayList<>());
private final List<String> myRequestVerbs = Collections.synchronizedList(new ArrayList<>()); private final List<String> myRequestVerbs = Collections.synchronizedList(new ArrayList<>());
@ -263,13 +279,31 @@ public class RestServerR4Helper extends BaseRestServerHelper implements BeforeEa
private HashMapResourceProvider<ConceptMap> myConceptMapResourceProvider; private HashMapResourceProvider<ConceptMap> myConceptMapResourceProvider;
private RestServerDstu3Helper.MyPlainProvider myPlainProvider; private RestServerDstu3Helper.MyPlainProvider myPlainProvider;
public MyRestfulServer(FhirContext theFhirContext) { private final boolean myInitialTransactionLatchEnabled;
public MyRestfulServer(FhirContext theFhirContext, boolean theInitialTransactionLatchEnabled) {
super(theFhirContext); super(theFhirContext);
myInitialTransactionLatchEnabled = theInitialTransactionLatchEnabled;
} }
public RestServerDstu3Helper.MyPlainProvider getPlainProvider() { public RestServerDstu3Helper.MyPlainProvider getPlainProvider() {
return myPlainProvider; return myPlainProvider;
} }
protected boolean isTransactionLatchEnabled() {
if (getPlainProvider() == null) {
return false;
}
return getPlainProvider().isTransactionLatchEnabled();
}
public void setTransactionLatchEnabled(boolean theTransactionLatchEnabled) {
getPlainProvider().setTransactionLatchEnabled(theTransactionLatchEnabled);
}
public void executeWithLatch(Runnable theRunnable) throws InterruptedException {
myPlainProvider.setExpectedCount(1);
theRunnable.run();
myPlainProvider.awaitExpected();
}
public void setFailNextPut(boolean theFailNextPut) { public void setFailNextPut(boolean theFailNextPut) {
myFailNextPut = theFailNextPut; myFailNextPut = theFailNextPut;
@ -282,7 +316,9 @@ public class RestServerR4Helper extends BaseRestServerHelper implements BeforeEa
provider.clearCounts(); provider.clearCounts();
} }
} }
if (isTransactionLatchEnabled()) {
myPlainProvider.clear(); myPlainProvider.clear();
}
myRequestUrls.clear(); myRequestUrls.clear();
myRequestVerbs.clear(); myRequestVerbs.clear();
} }
@ -364,7 +400,7 @@ public class RestServerR4Helper extends BaseRestServerHelper implements BeforeEa
myConceptMapResourceProvider = new MyHashMapResourceProvider(fhirContext, ConceptMap.class); myConceptMapResourceProvider = new MyHashMapResourceProvider(fhirContext, ConceptMap.class);
registerProvider(myConceptMapResourceProvider); registerProvider(myConceptMapResourceProvider);
myPlainProvider = new RestServerDstu3Helper.MyPlainProvider(); myPlainProvider = new RestServerDstu3Helper.MyPlainProvider(myInitialTransactionLatchEnabled);
registerProvider(myPlainProvider); registerProvider(myPlainProvider);
setPagingProvider(new FifoMemoryPagingProvider(20)); setPagingProvider(new FifoMemoryPagingProvider(20));

View File

@ -27,43 +27,33 @@ import ca.uhn.fhir.interceptor.api.IPointcut;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
// This class is primarily used for testing. // This class is primarily used for testing.
public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch { public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatch.class); private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatch.class);
private static final int DEFAULT_TIMEOUT_SECONDS = 10; private static final int DEFAULT_TIMEOUT_SECONDS = 10;
private static final FhirObjectPrinter ourFhirObjectToStringMapper = new FhirObjectPrinter();
private final String myName; private final String myName;
private final AtomicLong myLastInvoke = new AtomicLong();
private final AtomicReference<CountDownLatch> myCountdownLatch = new AtomicReference<>();
private final AtomicReference<String> myCountdownLatchSetStacktrace = new AtomicReference<>();
private final AtomicReference<List<String>> myFailures = new AtomicReference<>();
private final AtomicReference<List<HookParams>> myCalledWith = new AtomicReference<>();
private final IPointcut myPointcut; private final IPointcut myPointcut;
private boolean myStrict = true;
private final AtomicLong myLastInvoke = new AtomicLong();
private int myDefaultTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS; private int myDefaultTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS;
private int myInitialCount; private final List<PointcutLatchException> myUnexpectedInvocations = new ArrayList<>();
private boolean myExactMatch; private final AtomicReference<PointcutLatchSession> myPointcutLatchSession = new AtomicReference<>();
public PointcutLatch(IPointcut thePointcut) { public PointcutLatch(IPointcut thePointcut) {
this.myName = thePointcut.name(); this.myName = thePointcut.name();
myPointcut = thePointcut; myPointcut = thePointcut;
} }
public PointcutLatch(String theName) { public PointcutLatch(String theName) {
this.myName = theName; this.myName = theName;
myPointcut = null; myPointcut = null;
@ -80,9 +70,8 @@ public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
} }
// Useful for debugging when you need more time to step through a method // Useful for debugging when you need more time to step through a method
public PointcutLatch setDefaultTimeoutSeconds(int theDefaultTimeoutSeconds) { public void setDefaultTimeoutSeconds(int theDefaultTimeoutSeconds) {
myDefaultTimeoutSeconds = theDefaultTimeoutSeconds; myDefaultTimeoutSeconds = theDefaultTimeoutSeconds;
return this;
} }
@Override @Override
@ -91,12 +80,11 @@ public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
} }
public void setExpectedCount(int theCount, boolean theExactMatch) { public void setExpectedCount(int theCount, boolean theExactMatch) {
if (myCountdownLatch.get() != null) { if (myPointcutLatchSession.get() != null) {
String previousStack = myCountdownLatchSetStacktrace.get(); String previousStack = myPointcutLatchSession.get().getStackTrace();
throw new PointcutLatchException(Msg.code(1480) + "setExpectedCount() called before previous awaitExpected() completed. Previous set stack:\n" + previousStack); throw new PointcutLatchException(Msg.code(1480) + "setExpectedCount() called before previous awaitExpected() completed. Previous set stack:\n" + previousStack, myName);
} }
myExactMatch = theExactMatch; startSession(theCount, theExactMatch);
createLatch(theCount);
if (theExactMatch) { if (theExactMatch) {
ourLog.info("Expecting exactly {} calls to {} latch", theCount, myName); ourLog.info("Expecting exactly {} calls to {} latch", theCount, myName);
} else { } else {
@ -109,27 +97,11 @@ public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
} }
public boolean isSet() { public boolean isSet() {
return myCountdownLatch.get() != null; return myPointcutLatchSession.get() != null;
} }
private void createLatch(int theCount) { private void startSession(int theCount, boolean theExactMatch) {
myFailures.set(Collections.synchronizedList(new ArrayList<>())); myPointcutLatchSession.set(new PointcutLatchSession(getName(), theCount, theExactMatch));
myCalledWith.set(Collections.synchronizedList(new ArrayList<>()));
myCountdownLatch.set(new CountDownLatch(theCount));
try {
throw new Exception(Msg.code(1481));
} catch (Exception e) {
myCountdownLatchSetStacktrace.set(ExceptionUtils.getStackTrace(e));
}
myInitialCount = theCount;
}
private void addFailure(String failure) {
if (myFailures.get() != null) {
myFailures.get().add(failure);
} else {
throw new PointcutLatchException(Msg.code(1482) + "trying to set failure on latch that hasn't been created: " + failure);
}
} }
private String getName() { private String getName() {
@ -141,77 +113,59 @@ public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
return awaitExpectedWithTimeout(myDefaultTimeoutSeconds); return awaitExpectedWithTimeout(myDefaultTimeoutSeconds);
} }
public List<HookParams> awaitExpectedWithTimeout(int timeoutSecond) throws InterruptedException { public List<HookParams> awaitExpectedWithTimeout(int theTimeoutSecond) throws InterruptedException {
List<HookParams> retval = myCalledWith.get(); PointcutLatchSession initialSession = myPointcutLatchSession.get();
try {
CountDownLatch latch = myCountdownLatch.get();
Validate.notNull(latch, getName() + " awaitExpected() called before setExpected() called.");
if (!latch.await(timeoutSecond, TimeUnit.SECONDS)) {
throw new LatchTimedOutError(Msg.code(1483) + getName() + " timed out waiting " + timeoutSecond + " seconds for latch to countdown from " + myInitialCount + " to 0. Is " + latch.getCount() + ".");
}
// Defend against ConcurrentModificationException List<HookParams> retval;
String error = getName(); try {
if (myFailures.get() != null && myFailures.get().size() > 0) { if (isSet()) {
List<String> failures = new ArrayList<>(myFailures.get()); retval = myPointcutLatchSession.get().awaitExpectedWithTimeout(theTimeoutSecond);
if (failures.size() > 1) { Validate.isTrue(initialSession.equals(myPointcutLatchSession.get()), "Concurrency error: Latch session switched while waiting.");
error += " ERRORS: \n";
} else { } else {
error += " ERROR: "; throw new PointcutLatchException("awaitExpected() called before setExpected() called.", myName);
}
error += String.join("\n", failures);
error += "\nLatch called with values: " + toCalledWithString();
throw new AssertionError(Msg.code(1484) + error);
} }
} finally { } finally {
clear(); clear();
} }
Validate.isTrue(retval.equals(myCalledWith.get()), "Concurrency error: Latch switched while waiting.");
return retval; return retval;
} }
@Override @Override
public void clear() { public void clear() {
ourLog.debug("Clearing latch {}", getName()); ourLog.debug("Clearing latch {}", getName());
myCountdownLatch.set(null); checkExceptions();
myCountdownLatchSetStacktrace.set(null); myPointcutLatchSession.set(null);
myUnexpectedInvocations.clear();
} }
private String toCalledWithString() { private void checkExceptions() {
if (myCalledWith.get() == null) { if (!myStrict) {
return "[]"; return;
}
if (myUnexpectedInvocations.size() > 0) {
PointcutLatchException firstException = myUnexpectedInvocations.get(0);
int size = myUnexpectedInvocations.size();
if (firstException != null) {
throw new AssertionError(Msg.code(2344) + getName() + " had " + size + " exceptions. Throwing first one.", firstException);
} }
// Defend against ConcurrentModificationException
List<HookParams> calledWith = new ArrayList<>(myCalledWith.get());
if (calledWith.isEmpty()) {
return "[]";
} }
String retVal = "[ ";
retVal += calledWith.stream().flatMap(hookParams -> hookParams.values().stream()).map(ourFhirObjectToStringMapper).collect(Collectors.joining(", "));
return retVal + " ]";
} }
@Override @Override
public void invoke(IPointcut thePointcut, HookParams theArgs) { public void invoke(IPointcut thePointcut, HookParams theArgs) {
myLastInvoke.set(System.currentTimeMillis()); myLastInvoke.set(System.currentTimeMillis());
CountDownLatch latch = myCountdownLatch.get(); try {
if (myExactMatch) { PointcutLatchSession session = myPointcutLatchSession.get();
if (latch == null) { if (session == null) {
throw new PointcutLatchException(Msg.code(1485) + "invoke() for " + myName + " called outside of setExpectedCount() .. awaitExpected(). Probably got more invocations than expected or clear() was called before invoke() arrived with args: " + theArgs, theArgs); throw new PointcutLatchException(Msg.code(1485) + "invoke() called outside of setExpectedCount() .. awaitExpected(). Probably got more invocations than expected or clear() was called before invoke().", myName, theArgs);
} else if (latch.getCount() <= 0) {
addFailure("invoke() called when countdown was zero.");
} }
} else if (latch == null || latch.getCount() <= 0) { session.invoke(theArgs);
return; } catch (PointcutLatchException e) {
myUnexpectedInvocations.add(e);
throw e;
} }
if (myCalledWith.get() != null) {
myCalledWith.get().add(theArgs);
}
ourLog.debug("Called {} {} with {}", myName, latch, hookParamsToString(theArgs));
latch.countDown();
} }
public void call(Object arg) { public void call(Object arg) {
@ -222,46 +176,24 @@ public class PointcutLatch implements IAnonymousInterceptor, IPointcutLatch {
public String toString() { public String toString() {
return new ToStringBuilder(this) return new ToStringBuilder(this)
.append("name", myName) .append("name", myName)
.append("myCountdownLatch", myCountdownLatch) .append("pointCutLatchSession", myPointcutLatchSession)
// .append("myFailures", myFailures)
// .append("myCalledWith", myCalledWith)
.append("myInitialCount", myInitialCount)
.toString(); .toString();
} }
public Object getLatchInvocationParameter() { public void setStrict(Boolean theStrict) {
return getLatchInvocationParameter(myCalledWith.get()); myStrict = theStrict;
} }
@SuppressWarnings("unchecked") public static <T> T getInvocationParameterOfType(List<HookParams> theHookParams, Class<T> theType) {
public <T> T getLatchInvocationParameterOfType(Class<T> theType) { Validate.notNull(theHookParams);
List<HookParams> hookParamsList = myCalledWith.get(); Validate.isTrue(theHookParams.size() == 1, "Expected Pointcut to be invoked 1 time");
Validate.notNull(hookParamsList); HookParams hookParams = theHookParams.get(0);
Validate.isTrue(hookParamsList.size() == 1, "Expected Pointcut to be invoked 1 time");
HookParams hookParams = hookParamsList.get(0);
ListMultimap<Class<?>, Object> paramsForType = hookParams.getParamsForType(); ListMultimap<Class<?>, Object> paramsForType = hookParams.getParamsForType();
List<Object> objects = paramsForType.get(theType); List<Object> objects = paramsForType.get(theType);
Validate.isTrue(objects.size() == 1); Validate.isTrue(objects.size() == 1);
return (T) objects.get(0); return (T) objects.get(0);
} }
private class PointcutLatchException extends IllegalStateException {
private static final long serialVersionUID = 1372636272233536829L;
PointcutLatchException(String message, HookParams theArgs) {
super(getName() + ": " + message + " called with values: " + hookParamsToString(theArgs));
}
public PointcutLatchException(String message) {
super(getName() + ": " + message);
}
}
private static String hookParamsToString(HookParams hookParams) {
return hookParams.values().stream().map(ourFhirObjectToStringMapper).collect(Collectors.joining(", "));
}
public static Object getLatchInvocationParameter(List<HookParams> theHookParams) { public static Object getLatchInvocationParameter(List<HookParams> theHookParams) {
Validate.notNull(theHookParams); Validate.notNull(theHookParams);
Validate.isTrue(theHookParams.size() == 1, "Expected Pointcut to be invoked 1 time"); Validate.isTrue(theHookParams.size() == 1, "Expected Pointcut to be invoked 1 time");

View File

@ -0,0 +1,15 @@
package ca.uhn.test.concurrency;
import ca.uhn.fhir.interceptor.api.HookParams;
class PointcutLatchException extends IllegalStateException {
private static final long serialVersionUID = 1372636272233536829L;
PointcutLatchException(String theMessage, String theName, HookParams theArgs) {
super(theName + ": " + theMessage + " called with values:\n" + theArgs);
}
public PointcutLatchException(String theMessage, String theName) {
super(theName + ": " + theMessage);
}
}

View File

@ -0,0 +1,85 @@
package ca.uhn.test.concurrency;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* This object exists from the time setExpectedCount() is called until awaitExpected() completes. It tracks
* invocations on the PointcutLatch and throws exceptions when incorrect states are detected.
*/
public class PointcutLatchSession {
private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatchSession.class);
private final List<String> myFailures = Collections.synchronizedList(new ArrayList<>());
private final List<HookParams> myCalledWith = Collections.synchronizedList(new ArrayList<>());
private final CountDownLatch myCountdownLatch;
private final String myStacktrace;
private final String myName;
private final int myInitialCount;
private final boolean myExactMatch;
PointcutLatchSession(String theName, int theCount, boolean theExactMatch) {
myName = theName;
myInitialCount = theCount;
myCountdownLatch = new CountDownLatch(theCount);
myExactMatch = theExactMatch;
try {
throw new Exception(Msg.code(1481));
} catch (Exception e) {
myStacktrace = ExceptionUtils.getStackTrace(e);
}
}
String getStackTrace() {
return myStacktrace;
}
List<HookParams> awaitExpectedWithTimeout(int theTimeoutSecond) throws InterruptedException {
if (!myCountdownLatch.await(theTimeoutSecond, TimeUnit.SECONDS)) {
throw new LatchTimedOutError(Msg.code(1483) + myName + " timed out waiting " + theTimeoutSecond + " seconds for latch to countdown from " + myInitialCount + " to 0. Is " + myCountdownLatch.getCount() + ".");
}
// Defend against ConcurrentModificationException
String error = myName;
if (myFailures.size() > 0) {
List<String> failures = new ArrayList<>(myFailures);
if (failures.size() > 1) {
error += " ERRORS: \n";
} else {
error += " ERROR: ";
}
error += String.join("\n", failures);
error += "\nLatch called " + myCalledWith.size() + " times with values:\n" + StringUtils.join(myCalledWith, "\n");
throw new AssertionError(Msg.code(1484) + error);
}
return myCalledWith;
}
void invoke(HookParams theArgs) {
if (myExactMatch) {
if (myCountdownLatch.getCount() <= 0) {
myFailures.add("invoke() called when countdown was zero.");
}
}
myCalledWith.add(theArgs);
ourLog.debug("Called {} {} with {}", myName, myCountdownLatch, theArgs);
myCountdownLatch.countDown();
}
List<HookParams> getCalledWith() {
return myCalledWith;
}
}

View File

@ -0,0 +1,138 @@
package ca.uhn.test.concurrency;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.fail;
class PointcutLatchTest {
private static final Logger ourLog = LoggerFactory.getLogger(PointcutLatchTest.class);
public static final String TEST_LATCH_NAME = "test-latch-name";
private final ExecutorService myExecutorService = Executors.newSingleThreadExecutor();
private final PointcutLatch myPointcutLatch = new PointcutLatch(TEST_LATCH_NAME);
@AfterEach
void after() {
myPointcutLatch.clear();
myPointcutLatch.setStrict(true);
}
@Test
public void testInvokeSameThread() throws InterruptedException {
myPointcutLatch.setExpectedCount(1);
Thread thread = invoke();
assertEquals(thread, Thread.currentThread());
myPointcutLatch.awaitExpected();
}
private Thread invoke() {
ourLog.info("invoke");
myPointcutLatch.call(this);
return Thread.currentThread();
}
@Test
public void testInvokeDifferentThread() throws InterruptedException, ExecutionException {
myPointcutLatch.setExpectedCount(1);
Future<Thread> future = myExecutorService.submit(this::invoke);
myPointcutLatch.awaitExpected();
assertNotEquals(Thread.currentThread(), future.get());
}
@Test
public void testDoubleExpect() {
myPointcutLatch.setExpectedCount(1);
try {
myPointcutLatch.setExpectedCount(1);
fail();
} catch (PointcutLatchException e) {
assertThat(e.getMessage(), startsWith(TEST_LATCH_NAME + ": HAPI-1480: setExpectedCount() called before previous awaitExpected() completed. Previous set stack:"));
}
}
@Test
public void testNotCalled() throws InterruptedException {
myPointcutLatch.setExpectedCount(1);
try {
myPointcutLatch.awaitExpectedWithTimeout(1);
fail();
} catch (LatchTimedOutError e) {
assertEquals("HAPI-1483: test-latch-name PointcutLatch timed out waiting 1 seconds for latch to countdown from 1 to 0. Is 1.", e.getMessage());
}
}
@Test
public void testAwaitExpectedCalledBeforeExpect() throws InterruptedException {
try {
myPointcutLatch.awaitExpected();
fail();
} catch (PointcutLatchException e) {
assertEquals(TEST_LATCH_NAME + ": awaitExpected() called before setExpected() called.", e.getMessage());
}
}
@Test
public void testInvokeCalledBeforeExpect() {
try {
invoke();
fail();
} catch (PointcutLatchException e) {
assertThat(e.getMessage(), startsWith(TEST_LATCH_NAME + ": HAPI-1485: invoke() called outside of setExpectedCount() .. awaitExpected(). Probably got more invocations than expected or clear() was called before invoke()."));
}
// Don't blow up in the clear() called by @AfterEach
myPointcutLatch.setStrict(false);
}
@Test
public void testDoubleInvokeInexact() throws InterruptedException {
myPointcutLatch.setExpectedCount(1, false);
invoke();
invoke();
myPointcutLatch.awaitExpected();
}
@Test
public void testDoubleInvokeExact() throws InterruptedException {
myPointcutLatch.setExpectedCount(1);
invoke();
try {
invoke();
myPointcutLatch.awaitExpected();
fail();
} catch (AssertionError e) {
assertThat(e.getMessage(), startsWith("HAPI-1484: test-latch-name PointcutLatch ERROR: invoke() called when countdown was zero."));
}
}
@Test
public void testInvokeThenClear() throws ExecutionException, InterruptedException {
Future<Thread> future = myExecutorService.submit(this::invoke);
try {
future.get();
} catch (ExecutionException e) {
// This is the exception thrown on the invocation thread
assertThat(e.getMessage(), startsWith("ca.uhn.test.concurrency.PointcutLatchException: " + TEST_LATCH_NAME + ": HAPI-1485: invoke() called outside of setExpectedCount() .. awaitExpected(). Probably got more invocations than expected or clear() was called before invoke()."));
}
try {
myPointcutLatch.clear();
} catch (AssertionError e) {
// This is the exception the test thread gets
assertThat(e.getMessage(), startsWith("HAPI-2344: " + TEST_LATCH_NAME + " PointcutLatch had 1 exceptions. Throwing first one."));
}
// Don't blow up in the clear() called by @AfterEach
myPointcutLatch.setStrict(false);
}
}

View File

@ -117,7 +117,7 @@
<module>hapi-fhir-android</module> <module>hapi-fhir-android</module>
<module>hapi-fhir-cli</module> <module>hapi-fhir-cli</module>
<module>hapi-fhir-dist</module> <module>hapi-fhir-dist</module>
<module>tests/hapi-fhir-base-test-jaxrsserver-kotlin</module> <!-- <module>tests/hapi-fhir-base-test-jaxrsserver-kotlin</module>-->
<module>tests/hapi-fhir-base-test-mindeps-client</module> <module>tests/hapi-fhir-base-test-mindeps-client</module>
<module>tests/hapi-fhir-base-test-mindeps-server</module> <module>tests/hapi-fhir-base-test-mindeps-server</module>
<module>hapi-fhir-spring-boot</module> <module>hapi-fhir-spring-boot</module>