Resource change listener (#2191)

* init rev

* fix build

* Tweaked the POM config settings and also added some dependency exclusions (commented out for now).

* More re-factoring of the CQL Unit Tests.

* Removed a LogMessages.html file and a minor Unit Test change.

* Unit Tests.

* added debug logging to troubleshoot the dao that has no name

* added debug logging to troubleshoot the dao that has no name

* workaround to get past null dao resourceName issue

* fix jsons to get test to pass.  Test still fails with library id problem

* gitignore

* gitignore

* test passes!  Woohoo!

* undo troubleshooting logging

* added timer and logging.

* added asserts and time multiple evaluations and measure the average

* readme

* adding explanations

* added more explanatory notes

* measure 2 patients

* move pom to use cqf snapshot

* roughed out cache

* roughed out cache

* Added code to VersionChangeCache class.

* added tests

* added polling test

* wrote init version

* wrote init version

* optimized versioncache

* worked on getting tests to pass

* redesigned interfaces

* all tests pass

* fixmes

* fixmes

* rename param

* Added Unit Tests.

* javadoc

* Fixed the 2-Patient Unit Test.

* More Unit Test work.

* make ResourceVersionMap immutable

* Fixed a Unit Test that was failing intermittently by adding a new way to refresh the cache.

* Use a new method called doRefreshAllCaches(0) to force a refresh and have all Listeners called immediately.

* Cleaned up IVersionChangeListenerRegistry interface to make methods more clear and resolved all Unit Tests.

* disabled tests

* disabled tests

* removed unused test method

* fixed refresh logic and added asserts

* moved cache so it can be used by searchparamregistry

* Updated the Cql Unit Tests to be properly configured for Dstu3 or R4.

* started rewriting SearchParamRegistryImpl to use new cache
added init method to listener interface

* added fixmes

* adding tests

* tests pass

* added tests

* Fixed the way CqlProviderFactory Autowires Beans so it can work with both Dstu3 and R4 contexts.

* moar tests

* fix test

* work tests

* reverting unneccessary refactors

* undo unneccessary import changes to reduce MR size

* undo unneccessary import changes to reduce MR size

* Unit Test fixes...more to come...

* add unregister

* fix tests

* Changed ResourceVersionCache to use a Map of Maps.

* searchparam test

* test passes

* resolved fixme

* fixmies

* strengthen test asserts

* More Unit Test changes and added some FIMXME items.

* changed from long to changeresult

* renamed VersionChange -> ResourceChange

* fixed delete bug

* organize imports

* fix test

* add update test

* add test reset function

* fix stack overflow

* fix startup race condition (might still be intermittent)

* found the problem.  delete doesn't work because we can't look up the deleted resource to find out what its name is

* fixed regression

* abandoned idea of incrementally updating searchparam registry.  Rebuilding every time--it doesn't change that often.

* fix test

* begin with failing test

* test passes

* fixmes and javadoc

* fix test

* fixme

* fix test

* whack-a-mole.  Either subs pass or cql passes.  Something's fishy with the FhirContext

* fix subscription test initialization

* fix method name

* Re-factored the CqlProvider Unit Tests.

* changed ResourceChange API

* add interface

* add interface

* fix test

* add schedule test

* add doc

* init rev

* FIXME

* modify FhirContext change

* change fhirContext.getResourceTypes to lazy load

* converted subscriptions

* converted subscriptions

* begin with failing test

* test passes

* fix test

* test coverage

* test coverage

* test coverage

* test coverage

* good coverage now

* pre-review cleanup.  I think I found a bug.

* moved cache into listener entry
tests pass with fixmes

* fix test

* fix test

* fix test

* fixme

* FIXMEs

* merge cache and registry

* method reorg

* javadoc

* javadoc done.  all FIXMEs resolved.

* change log

* changes needed by cdr

* spring config cleanup

* james feedback

* james feedback

* might not work. Try moving resourcechangeconfig into searchparam config

* merge ResourceChangeListenerRegistryConfig.java into SearchParamConfig

* fix test

* fix SubscriptionLoader

* fix SubscriptionLoader

* create ResourceVersionMap from resources

* added cache handle interface

* fix test

* javadoc

* fix test

* fix test

* James feedback: clone searchparametermap

* fix startup

* fix test

* fix test

* fix intermittent

* pre-review cleanup

* FIXME

* final FIXME yay!

* Address a couple of my own reviw comments

Co-authored-by: Kevin Dougan <kevin.dougan@smilecdr.com>
Co-authored-by: jamesagnew <jamesagnew@gmail.com>
This commit is contained in:
Ken Stevens 2020-11-29 19:42:40 -05:00 committed by GitHub
parent 1e5def260c
commit 3d3242cf9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 2849 additions and 572 deletions

View File

@ -33,8 +33,10 @@ import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
@ -118,6 +120,7 @@ public class FhirContext {
private volatile RuntimeChildUndeclaredExtensionDefinition myRuntimeChildUndeclaredExtensionDefinition;
private IValidationSupport myValidationSupport;
private Map<FhirVersionEnum, Map<String, Class<? extends IBaseResource>>> myVersionToNameToResourceType = Collections.emptyMap();
private volatile Set<String> myResourceNames;
/**
* @deprecated It is recommended that you use one of the static initializer methods instead
@ -553,29 +556,31 @@ public class FhirContext {
* @since 5.1.0
*/
public Set<String> getResourceTypes() {
Set<String> resourceNames = new HashSet<>();
Set<String> resourceNames = myResourceNames;
if (resourceNames == null) {
resourceNames = buildResourceNames();
myResourceNames = resourceNames;
}
return resourceNames;
}
if (myNameToResourceDefinition.isEmpty()) {
Properties props = new Properties();
try {
props.load(myVersion.getFhirVersionPropertiesFile());
} catch (IOException theE) {
throw new ConfigurationException("Failed to load version properties file");
}
Enumeration<?> propNames = props.propertyNames();
while (propNames.hasMoreElements()) {
String next = (String) propNames.nextElement();
if (next.startsWith("resource.")) {
resourceNames.add(next.substring("resource.".length()).trim());
}
@Nonnull
private Set<String> buildResourceNames() {
Set<String> retVal = new HashSet<>();
Properties props = new Properties();
try (InputStream propFile = myVersion.getFhirVersionPropertiesFile()) {
props.load(propFile);
} catch (IOException e) {
throw new ConfigurationException("Failed to load version properties file", e);
}
Enumeration<?> propNames = props.propertyNames();
while (propNames.hasMoreElements()) {
String next = (String) propNames.nextElement();
if (next.startsWith("resource.")) {
retVal.add(next.substring("resource.".length()).trim());
}
}
for (RuntimeResourceDefinition next : myNameToResourceDefinition.values()) {
resourceNames.add(next.getName());
}
return Collections.unmodifiableSet(resourceNames);
return retVal;
}
/**

View File

@ -17,7 +17,9 @@ import org.hl7.fhir.instance.model.api.IIdType;
import java.math.BigDecimal;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/*
* #%L
@ -154,10 +156,15 @@ public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */IIdTy
myResourceType = theResourceType;
myUnqualifiedId = theId;
myUnqualifiedVersionId = StringUtils.defaultIfBlank(theVersionId, null);
myHaveComponentParts = true;
if (isBlank(myBaseUrl) && isBlank(myResourceType) && isBlank(myUnqualifiedId) && isBlank(myUnqualifiedVersionId)) {
myHaveComponentParts = false;
}
setHaveComponentParts(this);
}
public IdDt(IIdType theId) {
myBaseUrl = theId.getBaseUrl();
myResourceType = theId.getResourceType();
myUnqualifiedId = theId.getIdPart();
myUnqualifiedVersionId = theId.getVersionIdPart();
setHaveComponentParts(this);
}
/**
@ -167,6 +174,21 @@ public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */IIdTy
setValue(theUrl.getValueAsString());
}
/**
* Copy Constructor
*/
public IdDt(IdDt theIdDt) {
this(theIdDt.myBaseUrl, theIdDt.myResourceType, theIdDt.myUnqualifiedId, theIdDt.myUnqualifiedVersionId);
}
private void setHaveComponentParts(IdDt theIdDt) {
if (isBlank(myBaseUrl) && isBlank(myResourceType) && isBlank(myUnqualifiedId) && isBlank(myUnqualifiedVersionId)) {
myHaveComponentParts = false;
} else {
myHaveComponentParts = true;
}
}
@Override
public void applyTo(IBaseResource theResouce) {
if (theResouce == null) {
@ -642,7 +664,9 @@ public class IdDt extends UriDt implements /*IPrimitiveDatatype<String>, */IIdTy
value = existingValue;
}
return new IdDt(value + '/' + Constants.PARAM_HISTORY + '/' + theVersion);
IdDt retval = new IdDt(this);
retval.myUnqualifiedVersionId = theVersion;
return retval;
}
public static boolean isValidLong(String id) {

View File

@ -0,0 +1,10 @@
---
type: add
issue: 2191
title: "Added a new IResourceChangeListenerRegistry service and modified SearchParamRegistry and SubscriptionRegistry to use it.
This service contains an in-memory list of all registered {@link IResourceChangeListener} instances along
with their caches and other details needed to maintain those caches. Register an {@link IResourceChangeListener} instance
with this service to be notified when resources you care about are changed. This service quickly notifies listeners
of changes that happened on the local process and also eventually notifies listeners of changes that were made by
remote processes."

View File

@ -0,0 +1,40 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.stream.Collectors;
/**
* This service builds a map of resource ids to versions based on a SearchParameterMap.
* It is used by the in-memory resource-version cache to detect when resource versions have been changed by remote processes.
*/
@Service
public class ResourceVersionSvcDaoImpl implements IResourceVersionSvc {
private static final Logger myLogger = LoggerFactory.getLogger(ResourceVersionMap.class);
@Autowired
DaoRegistry myDaoRegistry;
@Autowired
IResourceTableDao myResourceTableDao;
@Nonnull
public ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap) {
IFhirResourceDao<?> dao = myDaoRegistry.getResourceDao(theResourceName);
List<Long> matchingIds = dao.searchForIds(theSearchParamMap, null).stream()
.map(ResourcePersistentId::getIdAsLong)
.collect(Collectors.toList());
return ResourceVersionMap.fromResourceTableEntities(myResourceTableDao.findAllById(matchingIds));
}
}

View File

@ -17,6 +17,8 @@ import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor;
import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc;
import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider;
import ca.uhn.fhir.jpa.bulk.svc.BulkDataExportSvcImpl;
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
import ca.uhn.fhir.jpa.cache.ResourceVersionSvcDaoImpl;
import ca.uhn.fhir.jpa.dao.HistoryBuilder;
import ca.uhn.fhir.jpa.dao.HistoryBuilderFactory;
import ca.uhn.fhir.jpa.dao.ISearchBuilder;
@ -88,9 +90,8 @@ import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor;
import org.hibernate.jpa.HibernatePersistenceProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.utilities.npm.BasePackageCacheManager;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
import org.hl7.fhir.utilities.npm.FilesystemPackageCacheManager;
import org.springframework.batch.core.configuration.annotation.BatchConfigurer;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.beans.factory.annotation.Autowired;
@ -148,6 +149,7 @@ import java.util.Date;
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.subscription.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.searchparam.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.empi.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.cache.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.starter.*"),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = "ca.uhn.fhir.jpa.batch.*")
})
@ -457,6 +459,11 @@ public abstract class BaseConfig {
return new HistoryBuilderFactory();
}
@Bean
public IResourceVersionSvc resourceVersionSvc() {
return new ResourceVersionSvcDaoImpl();
}
/* **************************************************************** *
* Prototype Beans Below *
* **************************************************************** */

View File

@ -49,6 +49,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableTransactionManagement
public class BaseDstu3Config extends BaseConfigDstu3Plus {
public static FhirContext ourFhirContext = FhirContext.forDstu3();
@Override
public FhirContext fhirContext() {
return fhirContextDstu3();
@ -63,7 +65,7 @@ public class BaseDstu3Config extends BaseConfigDstu3Plus {
@Bean
@Primary
public FhirContext fhirContextDstu3() {
FhirContext retVal = FhirContext.forDstu3();
FhirContext retVal = ourFhirContext;
// Don't strip versions in some places
ParserOptions parserOptions = retVal.getParserOptions();

View File

@ -21,11 +21,13 @@ package ca.uhn.fhir.jpa.dao;
*/
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
@ -39,15 +41,15 @@ public class DaoSearchParamProvider implements ISearchParamProvider {
@Override
public IBundleProvider search(SearchParameterMap theParams) {
return myDaoRegistry.getResourceDao(ResourceTypeEnum.SEARCHPARAMETER.getCode()).search(theParams);
return getSearchParamDao().search(theParams);
}
private IFhirResourceDao getSearchParamDao() {
return myDaoRegistry.getResourceDao(ResourceTypeEnum.SEARCHPARAMETER.getCode());
}
@Override
public int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval) {
int retVal = 0;
if (myDaoRegistry.getResourceDaoOrNull("SearchParameter") != null) {
retVal = theSearchParamRegistry.doRefresh(theRefreshInterval);
}
return retVal;
public IBaseResource read(IIdType theSearchParamId) {
return getSearchParamDao().read(theSearchParamId);
}
}

View File

@ -69,8 +69,8 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
@Autowired
protected PlatformTransactionManager myTransactionMgr;
private boolean myProcessDeferred = true;
final private List<TermCodeSystem> myDefferedCodeSystemsDeletions = Collections.synchronizedList(new ArrayList<>());
final private List<TermCodeSystemVersion> myDefferedCodeSystemVersionsDeletions = Collections.synchronizedList(new ArrayList<>());
final private List<TermCodeSystem> myDeferredCodeSystemsDeletions = Collections.synchronizedList(new ArrayList<>());
final private List<TermCodeSystemVersion> myDeferredCodeSystemVersionsDeletions = Collections.synchronizedList(new ArrayList<>());
final private List<TermConcept> myDeferredConcepts = Collections.synchronizedList(new ArrayList<>());
final private List<ValueSet> myDeferredValueSets = Collections.synchronizedList(new ArrayList<>());
final private List<ConceptMap> myDeferredConceptMaps = Collections.synchronizedList(new ArrayList<>());
@ -113,7 +113,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
public void deleteCodeSystem(TermCodeSystem theCodeSystem) {
theCodeSystem.setCodeSystemUri("urn:uuid:" + UUID.randomUUID().toString());
myCodeSystemDao.save(theCodeSystem);
myDefferedCodeSystemsDeletions.add(theCodeSystem);
myDeferredCodeSystemsDeletions.add(theCodeSystem);
}
@Override
@ -122,7 +122,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
List<TermCodeSystemVersion> codeSystemVersionsToDelete = myCodeSystemVersionDao.findByCodeSystemResourcePid(theCodeSystemToDelete.getResourceId());
for (TermCodeSystemVersion codeSystemVersionToDelete : codeSystemVersionsToDelete){
if (codeSystemVersionToDelete != null) {
myDefferedCodeSystemVersionsDeletions.add(codeSystemVersionToDelete);
myDeferredCodeSystemVersionsDeletions.add(codeSystemVersionToDelete);
}
}
TermCodeSystem codeSystemToDelete = myCodeSystemDao.findByResourcePid(theCodeSystemToDelete.getResourceId());
@ -223,11 +223,13 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
*/
@VisibleForTesting
public synchronized void clearDeferred() {
myProcessDeferred = true;
myDeferredValueSets.clear();
myDeferredConceptMaps.clear();
myDeferredConcepts.clear();
myDefferedCodeSystemsDeletions.clear();
myDeferredCodeSystemsDeletions.clear();
myConceptLinksToSaveLater.clear();
myDeferredCodeSystemVersionsDeletions.clear();
}
@Transactional(propagation = Propagation.NEVER)
@ -284,15 +286,15 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
private void processDeferredCodeSystemDeletions() {
for (TermCodeSystemVersion next : myDefferedCodeSystemVersionsDeletions) {
for (TermCodeSystemVersion next : myDeferredCodeSystemVersionsDeletions) {
myCodeSystemStorageSvc.deleteCodeSystemVersion(next);
}
myDefferedCodeSystemVersionsDeletions.clear();
for (TermCodeSystem next : myDefferedCodeSystemsDeletions) {
myDeferredCodeSystemVersionsDeletions.clear();
for (TermCodeSystem next : myDeferredCodeSystemsDeletions) {
myCodeSystemStorageSvc.deleteCodeSystem(next);
}
myDefferedCodeSystemsDeletions.clear();
myDeferredCodeSystemsDeletions.clear();
}
@Override
@ -322,7 +324,7 @@ public class TermDeferredStorageSvcImpl implements ITermDeferredStorageSvc {
}
private boolean isDeferredCodeSystemDeletions() {
return !myDefferedCodeSystemsDeletions.isEmpty() || !myDefferedCodeSystemVersionsDeletions.isEmpty();
return !myDeferredCodeSystemsDeletions.isEmpty() || !myDeferredCodeSystemVersionsDeletions.isEmpty();
}
private boolean isDeferredConcepts() {

View File

@ -0,0 +1,317 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.test.concurrency.IPointcutLatch;
import ca.uhn.test.concurrency.PointcutLatch;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
private static final long TEST_REFRESH_INTERVAL = DateUtils.MILLIS_PER_DAY;
@Autowired
ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
@Autowired
ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
private final static String RESOURCE_NAME = "Patient";
private TestCallback myMaleTestCallback = new TestCallback("MALE");
private TestCallback myFemaleTestCallback = new TestCallback("FEMALE");
@BeforeEach
public void before() {
myMaleTestCallback.clear();
}
@AfterEach
public void after() {
myResourceChangeListenerRegistry.clearListenersForUnitTest();
myResourceChangeListenerRegistry.clearCachesForUnitTest();
}
@Test
public void testRegisterListener() throws InterruptedException {
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, SearchParameterMap.newSynchronous(), myMaleTestCallback, TEST_REFRESH_INTERVAL);
Patient patient = createPatientWithInitLatch(null, myMaleTestCallback);
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
IdDt patientId = new IdDt(patient.getIdElement().toUnqualifiedVersionless());
patient.setActive(false);
patient.setGender(Enumerations.AdministrativeGender.FEMALE);
myPatientDao.update(patient);
myMaleTestCallback.setExpectedCount(1);
ResourceChangeResult result = cache.forceRefresh();
myMaleTestCallback.awaitExpected();
assertResult(result, 0, 1, 0);
assertEquals(2L, myMaleTestCallback.getUpdateResourceId().getVersionIdPartAsLong());
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
// Calling forceRefresh with no changes does not call listener
result = cache.forceRefresh();
assertResult(result, 0, 0, 0);
myMaleTestCallback.setExpectedCount(1);
myPatientDao.delete(patientId.toVersionless());
result = cache.forceRefresh();
assertResult(result, 0, 0, 1);
myMaleTestCallback.awaitExpected();
assertEquals(patientId, myMaleTestCallback.getDeletedResourceId());
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
}
@Test
public void testNonInMemorySearchParamCannotBeRegistered() {
try {
SearchParameterMap map = new SearchParameterMap();
map.setLastUpdated(new DateRangeParam("1965", "1970"));
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, map, myMaleTestCallback, TEST_REFRESH_INTERVAL);
fail();
} catch (IllegalArgumentException e) {
assertEquals("SearchParameterMap SearchParameterMap[] cannot be evaluated in-memory: Parameter: <_lastUpdated> Reason: Standard parameters not supported. Only search parameter maps that can be evaluated in-memory may be registered.", e.getMessage());
}
}
private void assertResult(ResourceChangeResult theResult, long theExpectedCreated, long theExpectedUpdated, long theExpectedDeleted) {
assertEquals(theExpectedCreated, theResult.created, "created results");
assertEquals(theExpectedUpdated, theResult.updated, "updated results");
assertEquals(theExpectedDeleted, theResult.deleted, "deleted results");
}
private void assertEmptyResult(ResourceChangeResult theResult) {
assertResult(theResult, 0, 0, 0);
}
private Patient createPatientWithInitLatch(Enumerations.AdministrativeGender theGender, TestCallback theTestCallback) throws InterruptedException {
Patient patient = new Patient();
patient.setActive(true);
if (theGender != null) {
patient.setGender(theGender);
}
theTestCallback.setInitExpectedCount(1);
IdDt patientId = createPatientAndRefreshCache(patient, theTestCallback, 1);
theTestCallback.awaitInitExpected();
List<IIdType> resourceIds = theTestCallback.getInitResourceIds();
assertThat(resourceIds, hasSize(1));
IIdType resourceId = resourceIds.get(0);
assertEquals(patientId.toString(), resourceId.toString());
assertEquals(1L, resourceId.getVersionIdPartAsLong());
return patient;
}
private IdDt createPatientAndRefreshCache(Patient thePatient, TestCallback theTestCallback, long theExpectedCount) throws InterruptedException {
IIdType retval = myPatientDao.create(thePatient).getId();
ResourceChangeResult result = myResourceChangeListenerCacheRefresher.forceRefreshAllCachesForUnitTest();
assertResult(result, theExpectedCount, 0, 0);
return new IdDt(retval);
}
@Test
public void testRegisterPolling() throws InterruptedException {
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, SearchParameterMap.newSynchronous(), myMaleTestCallback, TEST_REFRESH_INTERVAL);
Patient patient = createPatientWithInitLatch(null, myMaleTestCallback);
IdDt patientId = new IdDt(patient.getIdElement());
// Pretend we're on a different process in the cluster and so our cache doesn't have the cache yet
myResourceChangeListenerRegistry.clearCachesForUnitTest();
myMaleTestCallback.setExpectedCount(1);
ResourceChangeResult result = cache.forceRefresh();
assertResult(result, 1, 0, 0);
List<HookParams> calledWith = myMaleTestCallback.awaitExpected();
ResourceChangeEvent resourceChangeEvent = (ResourceChangeEvent) PointcutLatch.getLatchInvocationParameter(calledWith);
assertEquals(patientId, resourceChangeEvent.getCreatedResourceIds().get(0));
}
@Test
public void testRegisterInterceptorFor2Patients() throws InterruptedException {
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.MALE), myMaleTestCallback, TEST_REFRESH_INTERVAL);
createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
myMaleTestCallback.clear();
Patient patientFemale = new Patient();
patientFemale.setActive(true);
patientFemale.setGender(Enumerations.AdministrativeGender.FEMALE);
// NOTE: This scenario does not invoke the myTestCallback listener so just call the DAO directly
IIdType patientIdFemale = new IdDt(myPatientDao.create(patientFemale).getId());
ResourceChangeResult result = cache.forceRefresh();
assertEmptyResult(result);
assertNotNull(patientIdFemale.toString());
assertNull(myMaleTestCallback.getResourceChangeEvent());
}
@Test
public void testRegister2InterceptorsFor2Patients() throws InterruptedException {
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.MALE), myMaleTestCallback, TEST_REFRESH_INTERVAL);
createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
myMaleTestCallback.clear();
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.FEMALE), myFemaleTestCallback, TEST_REFRESH_INTERVAL);
createPatientWithInitLatch(Enumerations.AdministrativeGender.FEMALE, myFemaleTestCallback);
}
@Test
public void testRegisterPollingFor2Patients() throws InterruptedException {
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, createSearchParameterMap(Enumerations.AdministrativeGender.MALE), myMaleTestCallback, TEST_REFRESH_INTERVAL);
Patient patientMale = createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
IdDt patientIdMale = new IdDt(patientMale.getIdElement());
Patient patientFemale = new Patient();
patientFemale.setActive(true);
patientFemale.setGender(Enumerations.AdministrativeGender.FEMALE);
// NOTE: This scenario does not invoke the myTestCallback listener so just call the DAO directly
IIdType patientIdFemale = new IdDt(myPatientDao.create(patientFemale).getId());
ResourceChangeResult result = cache.forceRefresh();
assertEmptyResult(result);
assertNotNull(patientIdFemale.toString());
assertNull(myMaleTestCallback.getResourceChangeEvent());
// Pretend we're on a different process in the cluster and so our cache doesn't have the cache yet
myResourceChangeListenerRegistry.clearCachesForUnitTest();
myMaleTestCallback.setExpectedCount(1);
result = cache.forceRefresh();
// We should still only get one matching result
assertResult(result, 1, 0, 0);
List<HookParams> calledWith = myMaleTestCallback.awaitExpected();
ResourceChangeEvent resourceChangeEvent = (ResourceChangeEvent) PointcutLatch.getLatchInvocationParameter(calledWith);
assertEquals(patientIdMale, resourceChangeEvent.getCreatedResourceIds().get(0));
}
@Test
public void twoListenersSameMap() throws InterruptedException {
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
SearchParameterMap searchParameterMap = createSearchParameterMap(Enumerations.AdministrativeGender.MALE);
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, searchParameterMap, myMaleTestCallback, TEST_REFRESH_INTERVAL);
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
createPatientWithInitLatch(Enumerations.AdministrativeGender.MALE, myMaleTestCallback);
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
TestCallback otherTestCallback = new TestCallback("OTHER_MALE");
IResourceChangeListenerCache otherCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(RESOURCE_NAME, searchParameterMap, otherTestCallback, TEST_REFRESH_INTERVAL);
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
otherCache.forceRefresh();
assertEquals(2, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(myMaleTestCallback);
assertEquals(1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(otherTestCallback);
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
}
private SearchParameterMap createSearchParameterMap(Enumerations.AdministrativeGender theGender) {
return SearchParameterMap.newSynchronous().add(Patient.SP_GENDER, new TokenParam(null, theGender.toCode()));
}
private static class TestCallback implements IResourceChangeListener, IPointcutLatch {
private static final Logger ourLog = LoggerFactory.getLogger(TestCallback.class);
private final PointcutLatch myHandleLatch;
private final PointcutLatch myInitLatch;
private final String myName;
private IResourceChangeEvent myResourceChangeEvent;
private Collection<IIdType> myInitResourceIds;
public TestCallback(String theName) {
myName = theName;
myHandleLatch = new PointcutLatch(theName + " ResourceChangeListener handle called");
myInitLatch = new PointcutLatch(theName + " ResourceChangeListener init called");
}
@Override
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
ourLog.info("{} TestCallback.handleChange() called with {}", myName, theResourceChangeEvent);
myResourceChangeEvent = theResourceChangeEvent;
myHandleLatch.call(theResourceChangeEvent);
}
@Override
public void handleInit(Collection<IIdType> theResourceIds) {
myInitResourceIds = theResourceIds;
myInitLatch.call(theResourceIds);
}
@Override
public void clear() {
myResourceChangeEvent = null;
myInitResourceIds = null;
myHandleLatch.clear();
myInitLatch.clear();
}
@Override
public void setExpectedCount(int theCount) {
myHandleLatch.setExpectedCount(theCount);
}
@Override
public List<HookParams> awaitExpected() throws InterruptedException {
return myHandleLatch.awaitExpected();
}
public List<IIdType> getInitResourceIds() {
return new ArrayList<>(myInitResourceIds);
}
public IResourceChangeEvent getResourceChangeEvent() {
return myResourceChangeEvent;
}
public void setInitExpectedCount(int theCount) {
myInitLatch.setExpectedCount(theCount);
}
public void awaitInitExpected() throws InterruptedException {
myInitLatch.awaitExpected();
}
public IIdType getUpdateResourceId() {
assertThat(myResourceChangeEvent.getUpdatedResourceIds(), hasSize(1));
return myResourceChangeEvent.getUpdatedResourceIds().get(0);
}
public IIdType getDeletedResourceId() {
assertThat(myResourceChangeEvent.getDeletedResourceIds(), hasSize(1));
return myResourceChangeEvent.getDeletedResourceIds().get(0);
}
}
}

View File

@ -0,0 +1,32 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ResourceVersionCacheSvcTest extends BaseJpaR4Test {
@Autowired
IResourceVersionSvc myResourceVersionCacheSvc;
@Test
public void testGetVersionMap() {
Patient patient = new Patient();
patient.setActive(true);
IIdType patientId = myPatientDao.create(patient).getId();
ResourceVersionMap versionMap = myResourceVersionCacheSvc.getVersionMap("Patient", SearchParameterMap.newSynchronous());
assertEquals(1, versionMap.size());
assertEquals("1", versionMap.getVersion(patientId));
patient.setGender(Enumerations.AdministrativeGender.MALE);
myPatientDao.update(patient);
versionMap = myResourceVersionCacheSvc.getVersionMap("Patient", SearchParameterMap.newSynchronous());
assertEquals(1, versionMap.size());
assertEquals("2", versionMap.getVersion(patientId));
}
}

View File

@ -1,6 +1,20 @@
package ca.uhn.fhir.jpa.config;
import java.sql.*;
import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
@ -252,7 +266,7 @@ public class ConnectionWrapper implements Connection {
@Override
public void setReadOnly(boolean theReadOnly) throws SQLException {
ourLog.info("Setting connection as readonly");
ourLog.debug("Setting connection as readonly");
myWrap.setReadOnly(theReadOnly);
}

View File

@ -1,6 +1,5 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
@ -11,11 +10,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.dstu2.valueset.XPathUsageTypeEnum;
import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.param.DateParam;
import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam;
@ -24,7 +19,6 @@ import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import ca.uhn.fhir.util.TestUtil;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Appointment;
@ -56,11 +50,10 @@ import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.hl7.fhir.r4.model.Specimen;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.util.collections.ListUtil;
import org.springframework.transaction.TransactionStatus;
@ -1433,6 +1426,25 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
foundResources = toUnqualifiedVersionlessIdValues(results);
assertThat(foundResources, contains(patId.getValue()));
// Retire the param
fooSp.setId(spId);
fooSp.setStatus(Enumerations.PublicationStatus.RETIRED);
mySearchParameterDao.update(fooSp, mySrd);
mySearchParamRegistry.forceRefresh();
myResourceReindexingSvc.forceReindexingPass();
// Expect error since searchparam is now retired
map = new SearchParameterMap();
map.add("foo", new TokenParam(null, "male"));
try {
myPatientDao.search(map).size();
fail();
} catch (InvalidRequestException e) {
assertEquals("Unknown search parameter \"foo\" for resource type \"Patient\". Valid search parameters for this search are: [_id, _language, _lastUpdated, active, address, address-city, address-country, address-postalcode, address-state, address-use, birthdate, death-date, deceased, email, family, gender, general-practitioner, given, identifier, language, link, name, organization, phone, phonetic, telecom]", e.getMessage());
}
// Delete the param
mySearchParameterDao.delete(spId, mySrd);

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
@ -18,9 +19,9 @@ import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.collect.Sets;
import org.hl7.fhir.r4.model.BooleanType;
import org.hl7.fhir.r4.model.CodeableConcept;
@ -33,7 +34,6 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Quantity;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -350,13 +350,13 @@ public class SearchParamExtractorR4Test {
}
@Override
public boolean refreshCacheIfNecessary() {
public ResourceChangeResult refreshCacheIfNecessary() {
// nothing
return false;
return new ResourceChangeResult();
}
@Override
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
public ReadOnlySearchParamCache getActiveSearchParams() {
throw new UnsupportedOperationException();
}

View File

@ -1186,7 +1186,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
}
// FIXME KHS
@Test
public void testDeleteExpungeAllowed() {

View File

@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.AopTestUtils;
import java.util.ArrayList;
import java.util.List;
@ -44,9 +45,10 @@ public class TerminologySvcDeltaR4Test extends BaseJpaR4Test {
@AfterEach
public void after() {
myDaoConfig.setDeferIndexingForCodesystemsOfSize(new DaoConfig().getDeferIndexingForCodesystemsOfSize());
TermDeferredStorageSvcImpl termDeferredStorageSvc = AopTestUtils.getTargetObject(myTermDeferredStorageSvc);
termDeferredStorageSvc.clearDeferred();
}
@Test
public void testAddRootConcepts() {
createNotPresentCodeSystem();

View File

@ -30,7 +30,7 @@
<logger name="ca.uhn.fhir.jpa.dao" additivity="false" level="info">
<appender-ref ref="STDOUT" />
</logger>
<!-- Set to 'trace' to enable SQL logging -->
<logger name="org.hibernate.SQL" additivity="false" level="info">
<appender-ref ref="STDOUT" />

View File

@ -0,0 +1,20 @@
package ca.uhn.fhir.jpa.cache;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.List;
/**
* Registered IResourceChangeListener instances are called with this event to provide them with a list of ids of resources
* that match the search parameters and that changed from the last time they were checked.
*/
public interface IResourceChangeEvent {
List<IIdType> getCreatedResourceIds();
List<IIdType> getUpdatedResourceIds();
List<IIdType> getDeletedResourceIds();
/**
* @return true when all three lists are empty
*/
boolean isEmpty();
}

View File

@ -0,0 +1,22 @@
package ca.uhn.fhir.jpa.cache;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Collection;
/**
* To be notified of resource changes in the repository, implement this interface and register your instance with
* {@link IResourceChangeListenerRegistry}.
*/
public interface IResourceChangeListener {
/**
* This method is called within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of a listener registration
* @param theResourceIds the ids of all resources that match the search parameters the listener was registered with
*/
void handleInit(Collection<IIdType> theResourceIds);
/**
* Called by the {@link IResourceChangeListenerRegistry} when matching resource changes are detected
*/
void handleChange(IResourceChangeEvent theResourceChangeEvent);
}

View File

@ -0,0 +1,52 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import java.time.Instant;
/**
* This is a handle to the cache created by {@link IResourceChangeListenerRegistry} when a listener is registered.
* This this handle can be used to refresh the cache if required.
*/
public interface IResourceChangeListenerCache {
/**
* @return the search parameter map the listener was registered with
*/
SearchParameterMap getSearchParameterMap();
/**
* @return whether the cache has been initialized. (If not, the cache will be empty.)
*/
boolean isInitialized();
/**
* @return the name of the resource type the listener was registered with
*/
String getResourceName();
/**
* @return the next scheduled time the cache will search the repository, update its cache and notify
* its listener of any changes
*/
Instant getNextRefreshTime();
/**
* sets the nextRefreshTime to {@link Instant.MIN} so that the cache will be refreshed and listeners notified in another thread
* the next time cache refresh times are checked (every {@link ResourceChangeListenerCacheRefresherImpl.LOCAL_REFRESH_INTERVAL_MS}.
*/
void requestRefresh();
/**
* Refresh the cache immediately in the current thread and notify its listener if there are any changes
* @return counts of detected resource creates, updates and deletes
*/
ResourceChangeResult forceRefresh();
/**
* If nextRefreshTime is in the past, then update the cache with the current repository contents and notify its listener of any changes
* @return counts of detected resource creates, updates and deletes
*/
ResourceChangeResult refreshCacheIfNecessary();
// TODO KHS in the future support adding new listeners to existing caches
}

View File

@ -0,0 +1,24 @@
package ca.uhn.fhir.jpa.cache;
/**
* This is an internal service and is not intended to be used outside this package. Implementers should only directly
* call the {@link IResourceChangeListenerRegistry}.
*
* This service refreshes a {@link ResourceChangeListenerCache} cache and notifies its listener when
* the cache changes.
*/
public interface IResourceChangeListenerCacheRefresher {
/**
* If the current time is past the next refresh time of the registered listener, then check if any of its
* resources have changed and notify the listener accordingly
* @return an aggregate of all changes sent to all listeners
*/
ResourceChangeResult refreshExpiredCachesAndNotifyListeners();
/**
* Refresh the cache in this entry and notify the entry's listener if the cache changed
* @param theEntry the {@link IResourceChangeListenerCache} with the cache and the listener
* @return the number of resources that have been created, updated and deleted since the last time the cache was refreshed
*/
ResourceChangeResult refreshCacheAndNotifyListener(IResourceChangeListenerCache theEntry);
}

View File

@ -0,0 +1,63 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
/**
* This component holds an in-memory list of all registered {@link IResourceChangeListener} instances along
* with their caches and other details needed to maintain those caches. Register an {@link IResourceChangeListener} instance
* with this service to be notified when resources you care about are changed. This service quickly notifies listeners
* of changes that happened on the local process and also eventually notifies listeners of changes that were made by
* remote processes.
*/
public interface IResourceChangeListenerRegistry {
/**
* Register a listener in order to be notified whenever a resource matching the provided SearchParameterMap
* changes in any way. If the change happened on the same jvm process where this registry resides, then the listener will be called
* within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of the change happening. If the change happened
* on a different jvm process, then the listener will be called within the time specified in theRemoteRefreshIntervalMs parameter.
* @param theResourceName the type of the resource the listener should be notified about (e.g. "Subscription" or "SearchParameter")
* @param theSearchParameterMap the listener will only be notified of changes to resources that match this map
* @param theResourceChangeListener the listener that will be called whenever resource changes are detected
* @param theRemoteRefreshIntervalMs the number of milliseconds between checking the database for changed resources that match the search parameter map
* @throws ca.uhn.fhir.parser.DataFormatException if theResourceName is not a valid resource type in the FhirContext
* @throws IllegalArgumentException if theSearchParamMap cannot be evaluated in-memory
* @return RegisteredResourceChangeListener a handle to the created cache that can be used to manually refresh the cache if required
*/
IResourceChangeListenerCache registerResourceResourceChangeListener(String theResourceName, SearchParameterMap theSearchParameterMap, IResourceChangeListener theResourceChangeListener, long theRemoteRefreshIntervalMs);
/**
* Unregister a listener from this service
*
* @param theResourceChangeListener
*/
void unregisterResourceResourceChangeListener(IResourceChangeListener theResourceChangeListener);
/**
* Unregister a listener from this service using its cache handle
*
* @param theResourceChangeListenerCache
*/
void unregisterResourceResourceChangeListener(IResourceChangeListenerCache theResourceChangeListenerCache);
@VisibleForTesting
void clearListenersForUnitTest();
/**
*
* @param theCache
* @return true if theCache is registered
*/
boolean contains(IResourceChangeListenerCache theCache);
/**
* Called by the {@link ResourceChangeListenerRegistryInterceptor} when a resource is changed to invalidate matching
* caches so their listeners are notified the next time the caches are refreshed.
* @param theResource the resource that changed that might trigger a refresh
*/
void requestRefreshIfWatching(IBaseResource theResource);
}

View File

@ -0,0 +1,14 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import javax.annotation.Nonnull;
/**
* This interface is used by the {@link IResourceChangeListenerCacheRefresher} to read resources matching the provided
* search parameter map in the repository and compare them to caches stored in the {@link IResourceChangeListenerRegistry}.
*/
public interface IResourceVersionSvc {
@Nonnull
ResourceVersionMap getVersionMap(String theResourceName, SearchParameterMap theSearchParamMap);
}

View File

@ -0,0 +1,67 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.model.primitive.IdDt;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* An immutable list of resource ids that have been changed, updated, or deleted.
*/
public class ResourceChangeEvent implements IResourceChangeEvent {
private final List<IIdType> myCreatedResourceIds;
private final List<IIdType> myUpdatedResourceIds;
private final List<IIdType> myDeletedResourceIds;
private ResourceChangeEvent(Collection<IIdType> theCreatedResourceIds, Collection<IIdType> theUpdatedResourceIds, Collection<IIdType> theDeletedResourceIds) {
myCreatedResourceIds = copyFrom(theCreatedResourceIds);
myUpdatedResourceIds = copyFrom(theUpdatedResourceIds);
myDeletedResourceIds = copyFrom(theDeletedResourceIds);
}
public static ResourceChangeEvent fromCreatedResourceIds(Collection<IIdType> theCreatedResourceIds) {
return new ResourceChangeEvent(theCreatedResourceIds, Collections.emptyList(), Collections.emptyList());
}
public static ResourceChangeEvent fromCreatedUpdatedDeletedResourceIds(List<IIdType> theCreatedResourceIds, List<IIdType> theUpdatedResourceIds, List<IIdType> theDeletedResourceIds) {
return new ResourceChangeEvent(theCreatedResourceIds, theUpdatedResourceIds, theDeletedResourceIds);
}
private List<IIdType> copyFrom(Collection<IIdType> theResourceIds) {
ArrayList<IdDt> retval = new ArrayList<>();
theResourceIds.forEach(id -> retval.add(new IdDt(id)));
return Collections.unmodifiableList(retval);
}
@Override
public List<IIdType> getCreatedResourceIds() {
return myCreatedResourceIds;
}
@Override
public List<IIdType> getUpdatedResourceIds() {
return myUpdatedResourceIds;
}
@Override
public List<IIdType> getDeletedResourceIds() {
return myDeletedResourceIds;
}
public boolean isEmpty() {
return myCreatedResourceIds.isEmpty() && myUpdatedResourceIds.isEmpty() && myDeletedResourceIds.isEmpty();
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("myCreatedResourceIds", myCreatedResourceIds)
.append("myUpdatedResourceIds", myUpdatedResourceIds)
.append("myDeletedResourceIds", myDeletedResourceIds)
.toString();
}
}

View File

@ -0,0 +1,192 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
@Component
@Scope("prototype")
public class ResourceChangeListenerCache implements IResourceChangeListenerCache {
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerCache.class);
private static final int MAX_RETRIES = 60;
private static Instant ourNowForUnitTests;
@Autowired
IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher;
@Autowired
SearchParamMatcher mySearchParamMatcher;
private final String myResourceName;
private final IResourceChangeListener myResourceChangeListener;
private final SearchParameterMap mySearchParameterMap;
private final ResourceVersionCache myResourceVersionCache = new ResourceVersionCache();
private final long myRemoteRefreshIntervalMs;
private boolean myInitialized = false;
private Instant myNextRefreshTime = Instant.MIN;
public ResourceChangeListenerCache(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theSearchParameterMap, long theRemoteRefreshIntervalMs) {
myResourceName = theResourceName;
myResourceChangeListener = theResourceChangeListener;
mySearchParameterMap = SerializationUtils.clone(theSearchParameterMap);
myRemoteRefreshIntervalMs = theRemoteRefreshIntervalMs;
}
/**
* Request that the cache be refreshed at the next convenient time (in a different thread)
*/
@Override
public void requestRefresh() {
myNextRefreshTime = Instant.MIN;
}
/**
* Request that a cache be refreshed now, in the current thread
*/
@Override
public ResourceChangeResult forceRefresh() {
requestRefresh();
return refreshCacheWithRetry();
}
/**
* Refresh the cache if theResource matches our SearchParameterMap
* @param theResource
*/
public void requestRefreshIfWatching(IBaseResource theResource) {
if (matches(theResource)) {
requestRefresh();
}
}
public boolean matches(IBaseResource theResource) {
InMemoryMatchResult result = mySearchParamMatcher.match(mySearchParameterMap, theResource);
if (!result.supported()) {
// This should never happen since we enforce only in-memory SearchParamMaps at registration time
throw new IllegalStateException("Search Parameter Map " + mySearchParameterMap + " cannot be processed in-memory: " + result.getUnsupportedReason());
}
return result.matched();
}
@Override
public ResourceChangeResult refreshCacheIfNecessary() {
ResourceChangeResult retval = new ResourceChangeResult();
if (isTimeToRefresh()) {
retval = refreshCacheWithRetry();
}
return retval;
}
private boolean isTimeToRefresh() {
return myNextRefreshTime.isBefore(now());
}
private static Instant now() {
if (ourNowForUnitTests != null) {
return ourNowForUnitTests;
}
return Instant.now();
}
public ResourceChangeResult refreshCacheWithRetry() {
ResourceChangeResult retval;
try {
retval = refreshCacheAndNotifyListenersWithRetry();
} finally {
myNextRefreshTime = now().plus(Duration.ofMillis(myRemoteRefreshIntervalMs));
}
return retval;
}
private ResourceChangeResult refreshCacheAndNotifyListenersWithRetry() {
Retrier<ResourceChangeResult> refreshCacheRetrier = new Retrier<>(() -> {
synchronized (this) {
return myResourceChangeListenerCacheRefresher.refreshCacheAndNotifyListener(this);
}
}, MAX_RETRIES);
return refreshCacheRetrier.runWithRetry();
}
@Override
public Instant getNextRefreshTime() {
return myNextRefreshTime;
}
@Override
public SearchParameterMap getSearchParameterMap() {
return mySearchParameterMap;
}
@Override
public boolean isInitialized() {
return myInitialized;
}
public ResourceChangeListenerCache setInitialized(boolean theInitialized) {
myInitialized = theInitialized;
return this;
}
@Override
public String getResourceName() {
return myResourceName;
}
public ResourceVersionCache getResourceVersionCache() {
return myResourceVersionCache;
}
public IResourceChangeListener getResourceChangeListener() {
return myResourceChangeListener;
}
/**
* @param theTime has format like "12:34:56" i.e. HH:MM:SS
*/
@VisibleForTesting
public static void setNowForUnitTests(String theTime) {
if (theTime == null) {
ourNowForUnitTests = null;
return;
}
String datetime = "2020-11-16T" + theTime + "Z";
Clock clock = Clock.fixed(Instant.parse(datetime), ZoneId.systemDefault());
ourNowForUnitTests = Instant.now(clock);
}
@VisibleForTesting
Instant getNextRefreshTimeForUnitTest() {
return myNextRefreshTime;
}
@VisibleForTesting
public void clearForUnitTest() {
requestRefresh();
myResourceVersionCache.clear();
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("myResourceName", myResourceName)
.append("mySearchParameterMap", mySearchParameterMap)
.append("myInitialized", myInitialized)
.toString();
}
}

View File

@ -0,0 +1,16 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
@Service
public class ResourceChangeListenerCacheFactory {
@Autowired
ApplicationContext myApplicationContext;
public ResourceChangeListenerCache create(String theResourceName, SearchParameterMap theMap, IResourceChangeListener theResourceChangeListener, long theRemoteRefreshIntervalMs) {
return myApplicationContext.getBean(ResourceChangeListenerCache.class, theResourceName, theResourceChangeListener, theMap, theRemoteRefreshIntervalMs);
}
}

View File

@ -0,0 +1,151 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IIdType;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* This service refreshes the {@link IResourceChangeListenerCache} caches and notifies their listener when
* those caches change.
*
* Think of it like a Ferris Wheel that completes a full rotation once every 10 seconds.
* Every time a chair passes the bottom it checks to see if it's time to refresh that seat. If so,
* the Ferris Wheel stops, removes the riders, and loads a fresh cache for that chair, and calls the listener
* if any entries in the new cache are different from the last time that cache was loaded.
*/
@Service
public class ResourceChangeListenerCacheRefresherImpl implements IResourceChangeListenerCacheRefresher {
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerCacheRefresherImpl.class);
/**
* All cache entries are checked at this interval to see if they need to be refreshed
*/
static long LOCAL_REFRESH_INTERVAL_MS = 10 * DateUtils.MILLIS_PER_SECOND;
@Autowired
private ISchedulerService mySchedulerService;
@Autowired
private IResourceVersionSvc myResourceVersionSvc;
@Autowired
private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
@PostConstruct
public void start() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(getClass().getName());
jobDetail.setJobClass(Job.class);
mySchedulerService.scheduleLocalJob(LOCAL_REFRESH_INTERVAL_MS, jobDetail);
}
public static class Job implements HapiJob {
@Autowired
private IResourceChangeListenerCacheRefresher myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.refreshExpiredCachesAndNotifyListeners();
}
}
@Override
public ResourceChangeResult refreshExpiredCachesAndNotifyListeners() {
ResourceChangeResult retval = new ResourceChangeResult();
Iterator<ResourceChangeListenerCache> iterator = myResourceChangeListenerRegistry.iterator();
while (iterator.hasNext()) {
ResourceChangeListenerCache entry = iterator.next();
retval = retval.plus(entry.refreshCacheIfNecessary());
}
return retval;
}
@VisibleForTesting
public ResourceChangeResult forceRefreshAllCachesForUnitTest() {
ResourceChangeResult retval = new ResourceChangeResult();
Iterator<ResourceChangeListenerCache> iterator = myResourceChangeListenerRegistry.iterator();
while (iterator.hasNext()) {
IResourceChangeListenerCache entry = iterator.next();
retval = retval.plus(entry.forceRefresh());
}
return retval;
}
public ResourceChangeResult refreshCacheAndNotifyListener(IResourceChangeListenerCache theCache) {
ResourceChangeResult retval = new ResourceChangeResult();
if (!myResourceChangeListenerRegistry.contains(theCache)) {
ourLog.warn("Requesting cache refresh for unregistered listener {}. Aborting.", theCache);
return new ResourceChangeResult();
}
SearchParameterMap searchParamMap = theCache.getSearchParameterMap();
ResourceVersionMap newResourceVersionMap = myResourceVersionSvc.getVersionMap(theCache.getResourceName(), searchParamMap);
retval = retval.plus(notifyListener(theCache, newResourceVersionMap));
return retval;
}
/**
* Notify a listener with all matching resources if it hasn't been initialized yet, otherwise only notify it if
* any resources have changed
* @param theCache
* @param theNewResourceVersionMap the measured new resources
* @return the list of created, updated and deleted ids
*/
ResourceChangeResult notifyListener(IResourceChangeListenerCache theCache, ResourceVersionMap theNewResourceVersionMap) {
ResourceChangeResult retval;
ResourceChangeListenerCache cache = (ResourceChangeListenerCache) theCache;
IResourceChangeListener resourceChangeListener = cache.getResourceChangeListener();
if (theCache.isInitialized()) {
retval = compareLastVersionMapToNewVersionMapAndNotifyListenerOfChanges(resourceChangeListener, cache.getResourceVersionCache(), theNewResourceVersionMap);
} else {
cache.getResourceVersionCache().initialize(theNewResourceVersionMap);
resourceChangeListener.handleInit(theNewResourceVersionMap.getSourceIds());
retval = ResourceChangeResult.fromCreated(theNewResourceVersionMap.size());
cache.setInitialized(true);
}
return retval;
}
private ResourceChangeResult compareLastVersionMapToNewVersionMapAndNotifyListenerOfChanges(IResourceChangeListener theListener, ResourceVersionCache theOldResourceVersionCache, ResourceVersionMap theNewResourceVersionMap) {
// If the new ResourceVersionMap does not have the old key - delete it
List<IIdType> deletedIds = new ArrayList<>();
theOldResourceVersionCache.keySet()
.forEach(id -> {
if (!theNewResourceVersionMap.containsKey(id)) {
deletedIds.add(id);
}
});
deletedIds.forEach(theOldResourceVersionCache::removeResourceId);
List<IIdType> createdIds = new ArrayList<>();
List<IIdType> updatedIds = new ArrayList<>();
for (IIdType id : theNewResourceVersionMap.keySet()) {
String previousValue = theOldResourceVersionCache.put(id, theNewResourceVersionMap.get(id));
IIdType newId = id.withVersion(theNewResourceVersionMap.get(id));
if (previousValue == null) {
createdIds.add(newId);
} else if (!theNewResourceVersionMap.get(id).equals(previousValue)) {
updatedIds.add(newId);
}
}
IResourceChangeEvent resourceChangeEvent = ResourceChangeEvent.fromCreatedUpdatedDeletedResourceIds(createdIds, updatedIds, deletedIds);
if (!resourceChangeEvent.isEmpty()) {
theListener.handleChange(resourceChangeEvent);
}
return ResourceChangeResult.fromResourceChangeEvent(resourceChangeEvent);
}
}

View File

@ -0,0 +1,128 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Nonnull;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
* This component holds an in-memory list of all registered {@link IResourceChangeListener} instances along
* with their caches and other details needed to maintain those caches. Register an {@link IResourceChangeListener} instance
* with this service to be notified when resources you care about are changed. This service quickly notifies listeners
* of changes that happened on the local process and also eventually notifies listeners of changes that were made by
* remote processes.
*/
@Component
public class ResourceChangeListenerRegistryImpl implements IResourceChangeListenerRegistry {
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerRegistryImpl.class);
@Autowired
private FhirContext myFhirContext;
@Autowired
private InMemoryResourceMatcher myInMemoryResourceMatcher;
@Autowired
ResourceChangeListenerCacheFactory myResourceChangeListenerCacheFactory;
private final Queue<ResourceChangeListenerCache> myListenerEntries = new ConcurrentLinkedQueue<>();
/**
* Register a listener in order to be notified whenever a resource matching the provided SearchParameterMap
* changes in any way. If the change happened on the same jvm process where this registry resides, then the listener will be called
* within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS} of the change happening. If the change happened
* on a different jvm process, then the listener will be called within theRemoteRefreshIntervalMs.
* @param theResourceName the type of the resource the listener should be notified about (e.g. "Subscription" or "SearchParameter")
* @param theSearchParameterMap the listener will only be notified of changes to resources that match this map
* @param theResourceChangeListener the listener that will be called whenever resource changes are detected
* @param theRemoteRefreshIntervalMs the number of milliseconds between checking the database for changed resources that match the search parameter map
* @throws ca.uhn.fhir.parser.DataFormatException if theResourceName is not a valid resource type in our FhirContext
* @throws IllegalArgumentException if theSearchParamMap cannot be evaluated in-memory
* @return RegisteredResourceChangeListener that stores the resource id cache, and the next refresh time
*/
@Override
public IResourceChangeListenerCache registerResourceResourceChangeListener(String theResourceName, SearchParameterMap theSearchParameterMap, IResourceChangeListener theResourceChangeListener, long theRemoteRefreshIntervalMs) {
// Clone searchparameter map
RuntimeResourceDefinition resourceDef = myFhirContext.getResourceDefinition(theResourceName);
InMemoryMatchResult inMemoryMatchResult = myInMemoryResourceMatcher.canBeEvaluatedInMemory(theSearchParameterMap, resourceDef);
if (!inMemoryMatchResult.supported()) {
throw new IllegalArgumentException("SearchParameterMap " + theSearchParameterMap + " cannot be evaluated in-memory: " + inMemoryMatchResult.getUnsupportedReason() + ". Only search parameter maps that can be evaluated in-memory may be registered.");
}
return add(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
}
/**
* Unregister a listener from this service
*
* @param theResourceChangeListener
*/
@Override
public void unregisterResourceResourceChangeListener(IResourceChangeListener theResourceChangeListener) {
myListenerEntries.removeIf(l -> l.getResourceChangeListener().equals(theResourceChangeListener));
}
@Override
public void unregisterResourceResourceChangeListener(IResourceChangeListenerCache theResourceChangeListenerCache) {
myListenerEntries.remove(theResourceChangeListenerCache);
}
private IResourceChangeListenerCache add(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theMap, long theRemoteRefreshIntervalMs) {
ResourceChangeListenerCache retval = myResourceChangeListenerCacheFactory.create(theResourceName, theMap, theResourceChangeListener, theRemoteRefreshIntervalMs);
myListenerEntries.add(retval);
return retval;
}
@Nonnull
public Iterator<ResourceChangeListenerCache> iterator() {
return myListenerEntries.iterator();
}
public int size() {
return myListenerEntries.size();
}
@VisibleForTesting
public void clearCachesForUnitTest() {
myListenerEntries.forEach(ResourceChangeListenerCache::clearForUnitTest);
}
@Override
public boolean contains(IResourceChangeListenerCache theCache) {
return myListenerEntries.contains(theCache);
}
@VisibleForTesting
public int getResourceVersionCacheSizeForUnitTest() {
int retval = 0;
for (ResourceChangeListenerCache entry : myListenerEntries) {
retval += entry.getResourceVersionCache().size();
}
return retval;
}
@Override
public void requestRefreshIfWatching(IBaseResource theResource) {
String resourceName = myFhirContext.getResourceType(theResource);
for (ResourceChangeListenerCache entry : myListenerEntries) {
if (resourceName.equals(entry.getResourceName())) {
entry.requestRefreshIfWatching(theResource);
}
}
}
@Override
@VisibleForTesting
public void clearListenersForUnitTest() {
myListenerEntries.clear();
}
}

View File

@ -0,0 +1,56 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
/**
* This interceptor watches all resource changes on the server and compares them to the {@link IResourceChangeListenerCache}
* entries. If the resource matches the resource type and search parameter map of that entry, then the corresponding cache
* will be expired so it is refreshed and listeners are notified of that change within {@link ResourceChangeListenerCacheRefresherImpl#LOCAL_REFRESH_INTERVAL_MS}.
*/
@Service
public class ResourceChangeListenerRegistryInterceptor {
@Autowired
private IInterceptorService myInterceptorBroadcaster;
@Autowired
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
@PostConstruct
public void start() {
myInterceptorBroadcaster.registerInterceptor(this);
}
@PreDestroy
public void stop() {
myInterceptorBroadcaster.unregisterInterceptor(this);
}
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
public void created(IBaseResource theResource) {
handle(theResource);
}
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
public void deleted(IBaseResource theResource) {
handle(theResource);
}
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
public void updated(IBaseResource theResource) {
handle(theResource);
}
private void handle(IBaseResource theResource) {
if (theResource == null) {
return;
}
myResourceChangeListenerRegistry.requestRefreshIfWatching(theResource);
}
}

View File

@ -0,0 +1,46 @@
package ca.uhn.fhir.jpa.cache;
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
* An immutable object containing the count of resource creates, updates and deletes detected by a cache refresh operation.
* Used internally for testing.
*/
public class ResourceChangeResult {
public final long created;
public final long updated;
public final long deleted;
public ResourceChangeResult() {
created = 0;
updated = 0;
deleted = 0;
}
private ResourceChangeResult(long theCreated, long theUpdated, long theDeleted) {
created = theCreated;
updated = theUpdated;
deleted = theDeleted;
}
public static ResourceChangeResult fromCreated(int theCreated) {
return new ResourceChangeResult(theCreated, 0, 0);
}
public static ResourceChangeResult fromResourceChangeEvent(IResourceChangeEvent theResourceChangeEvent) {
return new ResourceChangeResult(theResourceChangeEvent.getCreatedResourceIds().size(), theResourceChangeEvent.getUpdatedResourceIds().size(), theResourceChangeEvent.getDeletedResourceIds().size());
}
public ResourceChangeResult plus(ResourceChangeResult theResult) {
return new ResourceChangeResult(created + theResult.created, updated + theResult.updated, deleted + theResult.deleted);
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("created", created)
.append("updated", updated)
.append("deleted", deleted)
.toString();
}
}

View File

@ -0,0 +1,51 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.model.primitive.IdDt;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* This maintains a mapping of resource id to resource version. We cache these in order to
* detect resources that were modified on remote servers in our cluster.
*/
public class ResourceVersionCache {
private final Map<IIdType, String> myVersionMap = new HashMap<>();
public void clear() {
myVersionMap.clear();
}
/**
* @param theResourceId
* @param theVersion
* @return previous value
*/
public String put(IIdType theResourceId, String theVersion) {
return myVersionMap.put(new IdDt(theResourceId).toVersionless(), theVersion);
}
public String getVersionForResourceId(IIdType theResourceId) {
return myVersionMap.get(new IdDt(theResourceId));
}
public String removeResourceId(IIdType theResourceId) {
return myVersionMap.remove(new IdDt(theResourceId));
}
public void initialize(ResourceVersionMap theResourceVersionMap) {
for (IIdType resourceId : theResourceVersionMap.keySet()) {
myVersionMap.put(resourceId, theResourceVersionMap.get(resourceId));
}
}
public int size() {
return myVersionMap.size();
}
public Set<IIdType> keySet() {
return myVersionMap.keySet();
}
}

View File

@ -0,0 +1,68 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.model.primitive.IdDt;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This immutable map holds a copy of current resource versions read from the repository.
*/
public class ResourceVersionMap {
private final Set<IIdType> mySourceIds = new HashSet<>();
private final Map<IIdType, String> myMap = new HashMap<>();
private ResourceVersionMap() {}
public static ResourceVersionMap fromResourceTableEntities(List<ResourceTable> theEntities) {
ResourceVersionMap retval = new ResourceVersionMap();
theEntities.forEach(entity -> retval.add(entity.getIdDt()));
return retval;
}
public static ResourceVersionMap fromResources(List<? extends IBaseResource> theResources) {
ResourceVersionMap retval = new ResourceVersionMap();
theResources.forEach(resource -> retval.add(resource.getIdElement()));
return retval;
}
public static ResourceVersionMap empty() {
return new ResourceVersionMap();
}
private void add(IIdType theId) {
IdDt id = new IdDt(theId);
mySourceIds.add(id);
myMap.put(id.toUnqualifiedVersionless(), id.getVersionIdPart());
}
public String getVersion(IIdType theResourceId) {
return myMap.get(new IdDt(theResourceId.toUnqualifiedVersionless()));
}
public int size() {
return myMap.size();
}
public Set<IIdType> keySet() {
return Collections.unmodifiableSet(myMap.keySet());
}
public Set<IIdType> getSourceIds() {
return Collections.unmodifiableSet(mySourceIds);
}
public String get(IIdType theId) {
return myMap.get(new IdDt(theId.toUnqualifiedVersionless()));
}
public boolean containsKey(IIdType theId) {
return myMap.containsKey(new IdDt(theId.toUnqualifiedVersionless()));
}
}

View File

@ -21,7 +21,15 @@ package ca.uhn.fhir.jpa.searchparam.config;
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCacheRefresher;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCache;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheFactory;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheRefresherImpl;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu2;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorDstu3;
@ -38,10 +46,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.context.annotation.Scope;
@Configuration
@EnableScheduling
public class SearchParamConfig {
@Autowired
@ -94,13 +101,32 @@ public class SearchParamConfig {
}
@Bean
public InMemoryResourceMatcher InMemoryResourceMatcher() {
public InMemoryResourceMatcher inMemoryResourceMatcher() {
return new InMemoryResourceMatcher();
}
@Bean
public SearchParamMatcher SearchParamMatcher() {
public SearchParamMatcher searchParamMatcher() {
return new SearchParamMatcher();
}
@Bean
IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
return new ResourceChangeListenerRegistryImpl();
}
@Bean
IResourceChangeListenerCacheRefresher resourceChangeListenerCacheRefresher() {
return new ResourceChangeListenerCacheRefresherImpl();
}
@Bean
ResourceChangeListenerCacheFactory registeredResourceListenerFactory() {
return new ResourceChangeListenerCacheFactory();
}
@Bean
@Scope("prototype")
ResourceChangeListenerCache registeredResourceChangeListener(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theSearchParameterMap, long theRemoteRefreshIntervalMs) {
return new ResourceChangeListenerCache(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
}
}

View File

@ -26,13 +26,25 @@ public class InMemoryMatchResult {
public static final String CHAIN = "Chained parameters are not supported";
public static final String PARAM = "Parameter not supported";
public static final String QUALIFIER = "Qualified parameter not supported";
public static final String LOCATION_NEAR = "Location.position near not supported";
public static final String LOCATION_NEAR = "Location.position near not supported";
private final boolean myMatch;
private final boolean myMatch;
/**
* True if it is expected that a search will be performed in-memory
*/
private final boolean mySupported;
/**
* if mySupported is false, then the parameter responsible for in-memory search not being supported
*/
private final String myUnsupportedParameter;
/**
* if mySupported is false, then the reason in-memory search is not supported
*/
private final String myUnsupportedReason;
/**
* Only used by CompositeInMemoryDaoSubscriptionMatcher to track whether we had to go
* out to the database to resolve the match.
*/
private boolean myInMemory = false;
private InMemoryMatchResult(boolean theMatch) {
@ -43,10 +55,10 @@ public class InMemoryMatchResult {
}
private InMemoryMatchResult(String theUnsupportedParameter, String theUnsupportedReason) {
this.myMatch = false;
this.mySupported = false;
this.myUnsupportedParameter = theUnsupportedParameter;
this.myUnsupportedReason = theUnsupportedReason;
myMatch = false;
mySupported = false;
myUnsupportedParameter = theUnsupportedParameter;
myUnsupportedReason = theUnsupportedReason;
}
public static InMemoryMatchResult successfulMatch() {

View File

@ -45,6 +45,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -83,17 +84,42 @@ public class InMemoryResourceMatcher {
return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.PARSE_FAIL);
}
searchParameterMap.clean();
if (searchParameterMap.getLastUpdated() != null) {
return match(searchParameterMap, theResource, resourceDefinition, theSearchParams);
}
/**
*
* @param theCriteria
* @return result.supported() will be true if theCriteria can be evaluated in-memory
*/
public InMemoryMatchResult canBeEvaluatedInMemory(String theCriteria) {
return match(theCriteria, null, null);
}
/**
*
* @param theSearchParameterMap
* @param theResourceDefinition
* @return result.supported() will be true if theSearchParameterMap can be evaluated in-memory
*/
public InMemoryMatchResult canBeEvaluatedInMemory(SearchParameterMap theSearchParameterMap, RuntimeResourceDefinition theResourceDefinition) {
return match(theSearchParameterMap, null, theResourceDefinition, null);
}
@Nonnull
public InMemoryMatchResult match(SearchParameterMap theSearchParameterMap, IBaseResource theResource, RuntimeResourceDefinition theResourceDefinition, ResourceIndexedSearchParams theSearchParams) {
if (theSearchParameterMap.getLastUpdated() != null) {
return InMemoryMatchResult.unsupportedFromParameterAndReason(Constants.PARAM_LASTUPDATED, InMemoryMatchResult.STANDARD_PARAMETER);
}
if (searchParameterMap.containsKey(Location.SP_NEAR)) {
if (theSearchParameterMap.containsKey(Location.SP_NEAR)) {
return InMemoryMatchResult.unsupportedFromReason(InMemoryMatchResult.LOCATION_NEAR);
}
for (Map.Entry<String, List<List<IQueryParameterType>>> entry : searchParameterMap.entrySet()) {
for (Map.Entry<String, List<List<IQueryParameterType>>> entry : theSearchParameterMap.entrySet()) {
String theParamName = entry.getKey();
List<List<IQueryParameterType>> theAndOrParams = entry.getValue();
InMemoryMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, resourceDefinition, theResource, theSearchParams);
InMemoryMatchResult result = matchIdsWithAndOr(theParamName, theAndOrParams, theResourceDefinition, theResource, theSearchParams);
if (!result.matched()) {
return result;
}

View File

@ -20,12 +20,19 @@ package ca.uhn.fhir.jpa.searchparam.matcher;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SearchParamMatcher {
@Autowired
private FhirContext myFhirContext;
@Autowired
private IndexedSearchParamExtractor myIndexedSearchParamExtractor;
@Autowired
@ -35,4 +42,13 @@ public class SearchParamMatcher {
ResourceIndexedSearchParams resourceIndexedSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, theRequest);
return myInMemoryResourceMatcher.match(theCriteria, theResource, resourceIndexedSearchParams);
}
public InMemoryMatchResult match(SearchParameterMap theSearchParameterMap, IBaseResource theResource) {
if (theSearchParameterMap.isEmpty()) {
return InMemoryMatchResult.successfulMatch();
}
ResourceIndexedSearchParams resourceIndexedSearchParams = myIndexedSearchParamExtractor.extractIndexedSearchParams(theResource, null);
RuntimeResourceDefinition resourceDefinition = myFhirContext.getResourceDefinition(theResource);
return myInMemoryResourceMatcher.match(theSearchParameterMap, theResource, resourceDefinition, resourceIndexedSearchParams);
}
}

View File

@ -23,9 +23,10 @@ package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
public interface ISearchParamProvider {
IBundleProvider search(SearchParameterMap theParams);
int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval);
IBaseResource read(IIdType theSearchParamId);
}

View File

@ -23,16 +23,13 @@ package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.rest.api.Constants;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
public interface ISearchParamRegistry {
@ -46,9 +43,12 @@ public interface ISearchParamRegistry {
*/
RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName);
boolean refreshCacheIfNecessary();
/**
* @return the number of search parameter entries changed
*/
ResourceChangeResult refreshCacheIfNecessary();
Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams();
ReadOnlySearchParamCache getActiveSearchParams();
Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName);
@ -79,9 +79,6 @@ public interface ISearchParamRegistry {
* such as <code>_id</code> and <code>_lastUpdated</code>.
*/
default Collection<String> getValidSearchParameterNamesIncludingMeta(String theResourceName) {
TreeSet<String> retVal = new TreeSet<>(getActiveSearchParams().get(theResourceName).keySet());
retVal.add(IAnyResource.SP_RES_ID);
retVal.add(Constants.PARAM_LASTUPDATED);
return retVal;
return getActiveSearchParams().getValidSearchParameterNamesIncludingMeta(theResourceName);
}
}

View File

@ -0,0 +1,147 @@
package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class JpaSearchParamCache {
private static final Logger ourLog = LoggerFactory.getLogger(JpaSearchParamCache.class);
private volatile Map<String, List<JpaRuntimeSearchParam>> myActiveUniqueSearchParams = Collections.emptyMap();
private volatile Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap();
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName) {
List<JpaRuntimeSearchParam> retval = myActiveUniqueSearchParams.get(theResourceName);
if (retval == null) {
retval = Collections.emptyList();
}
return retval;
}
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName, Set<String> theParamNames) {
Map<Set<String>, List<JpaRuntimeSearchParam>> paramNamesToParams = myActiveParamNamesToUniqueSearchParams.get(theResourceName);
if (paramNamesToParams == null) {
return Collections.emptyList();
}
List<JpaRuntimeSearchParam> retVal = paramNamesToParams.get(theParamNames);
if (retVal == null) {
retVal = Collections.emptyList();
}
return Collections.unmodifiableList(retVal);
}
void populateActiveSearchParams(IInterceptorService theInterceptorBroadcaster, IPhoneticEncoder theDefaultPhoneticEncoder, RuntimeSearchParamCache theActiveSearchParams) {
Map<String, List<JpaRuntimeSearchParam>> activeUniqueSearchParams = new HashMap<>();
Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> activeParamNamesToUniqueSearchParams = new HashMap<>();
Map<String, RuntimeSearchParam> idToRuntimeSearchParam = new HashMap<>();
List<JpaRuntimeSearchParam> jpaSearchParams = new ArrayList<>();
/*
* Loop through parameters and find JPA params
*/
for (String theResourceName : theActiveSearchParams.getResourceNameKeys()) {
Map<String, RuntimeSearchParam> searchParamMap = theActiveSearchParams.getSearchParamMap(theResourceName);
List<JpaRuntimeSearchParam> uniqueSearchParams = activeUniqueSearchParams.computeIfAbsent(theResourceName, k -> new ArrayList<>());
Collection<RuntimeSearchParam> nextSearchParamsForResourceName = searchParamMap.values();
ourLog.trace("Resource {} has {} params", theResourceName, searchParamMap.size());
for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) {
ourLog.trace("Resource {} has parameter {} with ID {}", theResourceName, nextCandidate.getName(), nextCandidate.getId());
if (nextCandidate.getId() != null) {
idToRuntimeSearchParam.put(nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate);
}
if (nextCandidate instanceof JpaRuntimeSearchParam) {
JpaRuntimeSearchParam nextCandidateCasted = (JpaRuntimeSearchParam) nextCandidate;
jpaSearchParams.add(nextCandidateCasted);
if (nextCandidateCasted.isUnique()) {
uniqueSearchParams.add(nextCandidateCasted);
}
}
setPhoneticEncoder(theDefaultPhoneticEncoder, nextCandidate);
}
}
ourLog.trace("Have {} search params loaded", idToRuntimeSearchParam.size());
Set<String> haveSeen = new HashSet<>();
for (JpaRuntimeSearchParam next : jpaSearchParams) {
if (!haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) {
continue;
}
Set<String> paramNames = new HashSet<>();
for (JpaRuntimeSearchParam.Component nextComponent : next.getComponents()) {
String nextRef = nextComponent.getReference().getReferenceElement().toUnqualifiedVersionless().getValue();
RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef);
if (componentTarget != null) {
next.getCompositeOf().add(componentTarget);
paramNames.add(componentTarget.getName());
} else {
String existingParams = idToRuntimeSearchParam
.keySet()
.stream()
.sorted()
.collect(Collectors.joining(", "));
String message = "Search parameter " + next.getId().toUnqualifiedVersionless().getValue() + " refers to unknown component " + nextRef + ", ignoring this parameter (valid values: " + existingParams + ")";
ourLog.warn(message);
// Interceptor broadcast: JPA_PERFTRACE_WARNING
HookParams params = new HookParams()
.add(RequestDetails.class, null)
.add(ServletRequestDetails.class, null)
.add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message));
theInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params);
}
}
if (next.getCompositeOf() != null) {
next.getCompositeOf().sort((theO1, theO2) -> StringUtils.compare(theO1.getName(), theO2.getName()));
for (String nextBase : next.getBase()) {
activeParamNamesToUniqueSearchParams.computeIfAbsent(nextBase, v -> new HashMap<>());
activeParamNamesToUniqueSearchParams.get(nextBase).computeIfAbsent(paramNames, t -> new ArrayList<>());
activeParamNamesToUniqueSearchParams.get(nextBase).get(paramNames).add(next);
}
}
}
ourLog.info("Have {} unique search params", activeParamNamesToUniqueSearchParams.size());
myActiveUniqueSearchParams = activeUniqueSearchParams;
myActiveParamNamesToUniqueSearchParams = activeParamNamesToUniqueSearchParams;
}
void setPhoneticEncoder(IPhoneticEncoder theDefaultPhoneticEncoder, RuntimeSearchParam searchParam) {
if ("phonetic".equals(searchParam.getName())) {
ourLog.debug("Setting search param {} on {} phonetic encoder to {}",
searchParam.getName(), searchParam.getPath(), theDefaultPhoneticEncoder == null ? "null" : theDefaultPhoneticEncoder.name());
searchParam.setPhoneticEncoder(theDefaultPhoneticEncoder);
}
}
}

View File

@ -0,0 +1,82 @@
package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.rest.api.Constants;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;
public class ReadOnlySearchParamCache {
private static final Logger ourLog = LoggerFactory.getLogger(ReadOnlySearchParamCache.class);
// resourceName -> searchParamName -> searchparam
protected final Map<String, Map<String, RuntimeSearchParam>> myMap;
ReadOnlySearchParamCache() {
myMap = new HashMap<>();
}
private ReadOnlySearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) {
myMap = theRuntimeSearchParamCache.myMap;
}
public static ReadOnlySearchParamCache fromFhirContext(FhirContext theFhirContext) {
ReadOnlySearchParamCache retval = new ReadOnlySearchParamCache();
Set<String> resourceNames = theFhirContext.getResourceTypes();
for (String resourceName : resourceNames) {
RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
String nextResourceName = nextResDef.getName();
HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
retval.myMap.put(nextResourceName, nameToParam);
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
nameToParam.put(nextSp.getName(), nextSp);
}
}
return retval;
}
public static ReadOnlySearchParamCache fromRuntimeSearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) {
return new ReadOnlySearchParamCache(theRuntimeSearchParamCache);
}
public Stream<RuntimeSearchParam> getSearchParamStream() {
return myMap.values().stream().flatMap(entry -> entry.values().stream());
}
protected Map<String, RuntimeSearchParam> getSearchParamMap(String theResourceName) {
Map<String, RuntimeSearchParam> retval = myMap.get(theResourceName);
if (retval == null) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(myMap.get(theResourceName));
}
public Collection<String> getValidSearchParameterNamesIncludingMeta(String theResourceName) {
TreeSet<String> retval;
Map<String, RuntimeSearchParam> searchParamMap = myMap.get(theResourceName);
if (searchParamMap == null) {
retval = new TreeSet<>();
} else {
retval = new TreeSet<>(searchParamMap.keySet());
}
retval.add(IAnyResource.SP_RES_ID);
retval.add(Constants.PARAM_LASTUPDATED);
return retval;
}
public int size() {
return myMap.size();
}
}

View File

@ -0,0 +1,63 @@
package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.RuntimeSearchParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class RuntimeSearchParamCache extends ReadOnlySearchParamCache {
private static final Logger ourLog = LoggerFactory.getLogger(RuntimeSearchParamCache.class);
protected RuntimeSearchParamCache() {
}
public static RuntimeSearchParamCache fromReadOnlySearchParmCache(ReadOnlySearchParamCache theBuiltInSearchParams) {
RuntimeSearchParamCache retval = new RuntimeSearchParamCache();
retval.putAll(theBuiltInSearchParams);
return retval;
}
public void add(String theResourceName, String theName, RuntimeSearchParam theSearchParam) {
getSearchParamMap(theResourceName).put(theName, theSearchParam);
}
public void remove(String theResourceName, String theName) {
if (!myMap.containsKey(theResourceName)) {
return;
}
myMap.get(theResourceName).remove(theName);
}
private void putAll(ReadOnlySearchParamCache theReadOnlySearchParamCache) {
Set<Map.Entry<String, Map<String, RuntimeSearchParam>>> builtInSps = theReadOnlySearchParamCache.myMap.entrySet();
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : builtInSps) {
for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) {
String nextResourceName = nextBuiltInEntry.getKey();
getSearchParamMap(nextResourceName).put(nextParam.getName(), nextParam);
}
ourLog.trace("Have {} built-in SPs for: {}", nextBuiltInEntry.getValue().size(), nextBuiltInEntry.getKey());
}
}
public RuntimeSearchParam get(String theResourceName, String theParamName) {
RuntimeSearchParam retVal = null;
Map<String, RuntimeSearchParam> params = myMap.get(theResourceName);
if (params != null) {
retVal = params.get(theParamName);
}
return retVal;
}
public Set<String> getResourceNameKeys() {
return myMap.keySet();
}
@Override
protected Map<String, RuntimeSearchParam> getSearchParamMap(String theResourceName) {
return myMap.computeIfAbsent(theResourceName, k -> new HashMap<>());
}
}

View File

@ -24,39 +24,31 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.api.Interceptor;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCache;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.SearchParameterUtil;
import ca.uhn.fhir.util.StopWatch;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.quartz.JobExecutionContext;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -64,12 +56,11 @@ import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class SearchParamRegistryImpl implements ISearchParamRegistry {
private static final int MAX_MANAGED_PARAM_COUNT = 10000;
public class SearchParamRegistryImpl implements ISearchParamRegistry, IResourceChangeListener {
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamRegistryImpl.class);
private static final int MAX_RETRIES = 60; // 5 minutes
private static long REFRESH_INTERVAL = 60 * DateUtils.MILLIS_PER_MINUTE;
private static final int MAX_MANAGED_PARAM_COUNT = 10000;
private static long REFRESH_INTERVAL = DateUtils.MILLIS_PER_HOUR;
@Autowired
private ModelConfig myModelConfig;
@Autowired
@ -77,277 +68,139 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
@Autowired
private FhirContext myFhirContext;
@Autowired
private ISchedulerService mySchedulerService;
@Autowired
private SearchParameterCanonicalizer mySearchParameterCanonicalizer;
@Autowired
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
private Map<String, Map<String, RuntimeSearchParam>> myBuiltInSearchParams;
private IPhoneticEncoder myPhoneticEncoder;
private volatile Map<String, List<JpaRuntimeSearchParam>> myActiveUniqueSearchParams = Collections.emptyMap();
private volatile Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> myActiveParamNamesToUniqueSearchParams = Collections.emptyMap();
private volatile Map<String, Map<String, RuntimeSearchParam>> myActiveSearchParams;
private volatile long myLastRefresh;
private volatile ReadOnlySearchParamCache myBuiltInSearchParams;
private volatile IPhoneticEncoder myPhoneticEncoder;
private volatile JpaSearchParamCache myJpaSearchParamCache = new JpaSearchParamCache();
private volatile RuntimeSearchParamCache myActiveSearchParams;
@Autowired
private IInterceptorService myInterceptorBroadcaster;
private RefreshSearchParameterCacheOnUpdate myInterceptor;
private IResourceChangeListenerCache myResourceChangeListenerCache;
@Override
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
requiresActiveSearchParams();
RuntimeSearchParam retVal = null;
Map<String, RuntimeSearchParam> params = myActiveSearchParams.get(theResourceName);
if (params != null) {
retVal = params.get(theParamName);
// Can still be null in unit test scenarios
if (myActiveSearchParams != null) {
return myActiveSearchParams.get(theResourceName, theParamName);
} else {
return null;
}
return retVal;
}
@Override
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
requiresActiveSearchParams();
return getActiveSearchParams().get(theResourceName);
return getActiveSearchParams().getSearchParamMap(theResourceName);
}
private void requiresActiveSearchParams() {
if (myActiveSearchParams == null) {
refreshCacheWithRetry();
myResourceChangeListenerCache.forceRefresh();
}
}
@Override
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName) {
List<JpaRuntimeSearchParam> retVal = myActiveUniqueSearchParams.get(theResourceName);
if (retVal == null) {
retVal = Collections.emptyList();
}
return retVal;
return myJpaSearchParamCache.getActiveUniqueSearchParams(theResourceName);
}
@Override
public List<JpaRuntimeSearchParam> getActiveUniqueSearchParams(String theResourceName, Set<String> theParamNames) {
Map<Set<String>, List<JpaRuntimeSearchParam>> paramNamesToParams = myActiveParamNamesToUniqueSearchParams.get(theResourceName);
if (paramNamesToParams == null) {
return Collections.emptyList();
}
List<JpaRuntimeSearchParam> retVal = paramNamesToParams.get(theParamNames);
if (retVal == null) {
retVal = Collections.emptyList();
}
return Collections.unmodifiableList(retVal);
return myJpaSearchParamCache.getActiveUniqueSearchParams(theResourceName, theParamNames);
}
private Map<String, Map<String, RuntimeSearchParam>> getBuiltInSearchParams() {
private void rebuildActiveSearchParams() {
ourLog.info("Rebuilding SearchParamRegistry");
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
int size = allSearchParamsBp.size();
ourLog.trace("Loaded {} search params from the DB", size);
// Just in case..
if (size >= MAX_MANAGED_PARAM_COUNT) {
ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
size = MAX_MANAGED_PARAM_COUNT;
}
List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, size);
initializeActiveSearchParams(allSearchParams);
}
private void initializeActiveSearchParams(Collection<IBaseResource> theJpaSearchParams) {
StopWatch sw = new StopWatch();
RuntimeSearchParamCache searchParams = RuntimeSearchParamCache.fromReadOnlySearchParmCache(getBuiltInSearchParams());
long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams);
ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
removeInactiveSearchParams(searchParams);
myActiveSearchParams = searchParams;
myJpaSearchParamCache.populateActiveSearchParams(myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams);
ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis());
}
private ReadOnlySearchParamCache getBuiltInSearchParams() {
if (myBuiltInSearchParams == null) {
myBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(myFhirContext);
}
return myBuiltInSearchParams;
}
private Map<String, RuntimeSearchParam> getSearchParamMap(Map<String, Map<String, RuntimeSearchParam>> searchParams, String theResourceName) {
Map<String, RuntimeSearchParam> retVal = searchParams.computeIfAbsent(theResourceName, k -> new HashMap<>());
return retVal;
private void removeInactiveSearchParams(RuntimeSearchParamCache theSearchParams) {
for (String resourceName : theSearchParams.getResourceNameKeys()) {
Map<String, RuntimeSearchParam> map = theSearchParams.getSearchParamMap(resourceName);
map.entrySet().removeIf(entry -> entry.getValue().getStatus() != RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE);
}
}
private void populateActiveSearchParams(Map<String, Map<String, RuntimeSearchParam>> theActiveSearchParams) {
Map<String, List<JpaRuntimeSearchParam>> activeUniqueSearchParams = new HashMap<>();
Map<String, Map<Set<String>, List<JpaRuntimeSearchParam>>> activeParamNamesToUniqueSearchParams = new HashMap<>();
Map<String, RuntimeSearchParam> idToRuntimeSearchParam = new HashMap<>();
List<JpaRuntimeSearchParam> jpaSearchParams = new ArrayList<>();
/*
* Loop through parameters and find JPA params
*/
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextResourceNameToEntries : theActiveSearchParams.entrySet()) {
List<JpaRuntimeSearchParam> uniqueSearchParams = activeUniqueSearchParams.computeIfAbsent(nextResourceNameToEntries.getKey(), k -> new ArrayList<>());
Collection<RuntimeSearchParam> nextSearchParamsForResourceName = nextResourceNameToEntries.getValue().values();
ourLog.trace("Resource {} has {} params", nextResourceNameToEntries.getKey(), nextResourceNameToEntries.getValue().size());
for (RuntimeSearchParam nextCandidate : nextSearchParamsForResourceName) {
ourLog.trace("Resource {} has parameter {} with ID {}", nextResourceNameToEntries.getKey(), nextCandidate.getName(), nextCandidate.getId());
if (nextCandidate.getId() != null) {
idToRuntimeSearchParam.put(nextCandidate.getId().toUnqualifiedVersionless().getValue(), nextCandidate);
}
if (nextCandidate instanceof JpaRuntimeSearchParam) {
JpaRuntimeSearchParam nextCandidateCasted = (JpaRuntimeSearchParam) nextCandidate;
jpaSearchParams.add(nextCandidateCasted);
if (nextCandidateCasted.isUnique()) {
uniqueSearchParams.add(nextCandidateCasted);
}
}
setPhoneticEncoder(nextCandidate);
}
private long overrideBuiltinSearchParamsWithActiveJpaSearchParams(RuntimeSearchParamCache theSearchParamCache, Collection<IBaseResource> theSearchParams) {
if (!myModelConfig.isDefaultSearchParamsCanBeOverridden() || theSearchParams == null) {
return 0;
}
ourLog.trace("Have {} search params loaded", idToRuntimeSearchParam.size());
long retval = 0;
for (IBaseResource searchParam : theSearchParams) {
retval += overrideSearchParam(theSearchParamCache, searchParam);
}
return retval;
}
Set<String> haveSeen = new HashSet<>();
for (JpaRuntimeSearchParam next : jpaSearchParams) {
if (!haveSeen.add(next.getId().toUnqualifiedVersionless().getValue())) {
private long overrideSearchParam(RuntimeSearchParamCache theSearchParams, IBaseResource theSearchParameter) {
if (theSearchParameter == null) {
return 0;
}
RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(theSearchParameter);
if (runtimeSp == null) {
return 0;
}
if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) {
return 0;
}
long retval = 0;
for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, theSearchParameter)) {
if (isBlank(nextBaseName)) {
continue;
}
Set<String> paramNames = new HashSet<>();
for (JpaRuntimeSearchParam.Component nextComponent : next.getComponents()) {
String nextRef = nextComponent.getReference().getReferenceElement().toUnqualifiedVersionless().getValue();
RuntimeSearchParam componentTarget = idToRuntimeSearchParam.get(nextRef);
if (componentTarget != null) {
next.getCompositeOf().add(componentTarget);
paramNames.add(componentTarget.getName());
} else {
String existingParams = idToRuntimeSearchParam
.keySet()
.stream()
.sorted()
.collect(Collectors.joining(", "));
String message = "Search parameter " + next.getId().toUnqualifiedVersionless().getValue() + " refers to unknown component " + nextRef + ", ignoring this parameter (valid values: " + existingParams + ")";
ourLog.warn(message);
// Interceptor broadcast: JPA_PERFTRACE_WARNING
HookParams params = new HookParams()
.add(RequestDetails.class, null)
.add(ServletRequestDetails.class, null)
.add(StorageProcessingMessage.class, new StorageProcessingMessage().setMessage(message));
myInterceptorBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_WARNING, params);
}
}
if (next.getCompositeOf() != null) {
next.getCompositeOf().sort((theO1, theO2) -> StringUtils.compare(theO1.getName(), theO2.getName()));
for (String nextBase : next.getBase()) {
activeParamNamesToUniqueSearchParams.computeIfAbsent(nextBase, v -> new HashMap<>());
activeParamNamesToUniqueSearchParams.get(nextBase).computeIfAbsent(paramNames, t -> new ArrayList<>());
activeParamNamesToUniqueSearchParams.get(nextBase).get(paramNames).add(next);
}
}
Map<String, RuntimeSearchParam> searchParamMap = theSearchParams.getSearchParamMap(nextBaseName);
String name = runtimeSp.getName();
ourLog.debug("Adding search parameter {}.{} to SearchParamRegistry", nextBaseName, StringUtils.defaultString(name, "[composite]"));
searchParamMap.put(name, runtimeSp);
retval++;
}
ourLog.trace("Have {} unique search params", activeParamNamesToUniqueSearchParams.size());
myActiveUniqueSearchParams = activeUniqueSearchParams;
myActiveParamNamesToUniqueSearchParams = activeParamNamesToUniqueSearchParams;
return retval;
}
@PostConstruct
public void start() {
myBuiltInSearchParams = createBuiltInSearchParamMap(myFhirContext);
myInterceptor = new RefreshSearchParameterCacheOnUpdate();
myInterceptorBroadcaster.registerInterceptor(myInterceptor);
}
@PreDestroy
public void stop() {
myInterceptorBroadcaster.unregisterInterceptor(myInterceptor);
}
public int doRefresh(long theRefreshInterval) {
if (System.currentTimeMillis() - theRefreshInterval > myLastRefresh) {
StopWatch sw = new StopWatch();
Map<String, Map<String, RuntimeSearchParam>> searchParams = new HashMap<>();
Set<Map.Entry<String, Map<String, RuntimeSearchParam>>> builtInSps = getBuiltInSearchParams().entrySet();
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextBuiltInEntry : builtInSps) {
for (RuntimeSearchParam nextParam : nextBuiltInEntry.getValue().values()) {
String nextResourceName = nextBuiltInEntry.getKey();
getSearchParamMap(searchParams, nextResourceName).put(nextParam.getName(), nextParam);
}
ourLog.trace("Have {} built-in SPs for: {}", nextBuiltInEntry.getValue().size(), nextBuiltInEntry.getKey());
}
SearchParameterMap params = new SearchParameterMap();
params.setLoadSynchronousUpTo(MAX_MANAGED_PARAM_COUNT);
IBundleProvider allSearchParamsBp = mySearchParamProvider.search(params);
int size = allSearchParamsBp.size();
ourLog.trace("Loaded {} search params from the DB", size);
// Just in case..
if (size >= MAX_MANAGED_PARAM_COUNT) {
ourLog.warn("Unable to support >" + MAX_MANAGED_PARAM_COUNT + " search params!");
size = MAX_MANAGED_PARAM_COUNT;
}
int overriddenCount = 0;
List<IBaseResource> allSearchParams = allSearchParamsBp.getResources(0, size);
for (IBaseResource nextResource : allSearchParams) {
IBaseResource nextSp = nextResource;
if (nextSp == null) {
continue;
}
RuntimeSearchParam runtimeSp = mySearchParameterCanonicalizer.canonicalizeSearchParameter(nextSp);
if (runtimeSp == null) {
continue;
}
if (runtimeSp.getStatus() == RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT) {
continue;
}
for (String nextBaseName : SearchParameterUtil.getBaseAsStrings(myFhirContext, nextSp)) {
if (isBlank(nextBaseName)) {
continue;
}
Map<String, RuntimeSearchParam> searchParamMap = getSearchParamMap(searchParams, nextBaseName);
String name = runtimeSp.getName();
if (!searchParamMap.containsKey(name) || myModelConfig.isDefaultSearchParamsCanBeOverridden()) {
searchParamMap.put(name, runtimeSp);
overriddenCount++;
}
}
}
ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
Map<String, Map<String, RuntimeSearchParam>> activeSearchParams = new HashMap<>();
for (Map.Entry<String, Map<String, RuntimeSearchParam>> nextEntry : searchParams.entrySet()) {
for (RuntimeSearchParam nextSp : nextEntry.getValue().values()) {
String nextName = nextSp.getName();
if (nextSp.getStatus() != RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE) {
nextSp = null;
}
if (!activeSearchParams.containsKey(nextEntry.getKey())) {
activeSearchParams.put(nextEntry.getKey(), new HashMap<>());
}
if (activeSearchParams.containsKey(nextEntry.getKey())) {
ourLog.debug("Replacing existing/built in search param {}:{} with new one", nextEntry.getKey(), nextName);
}
if (nextSp != null) {
activeSearchParams.get(nextEntry.getKey()).put(nextName, nextSp);
} else {
activeSearchParams.get(nextEntry.getKey()).remove(nextName);
}
}
}
myActiveSearchParams = activeSearchParams;
populateActiveSearchParams(activeSearchParams);
myLastRefresh = System.currentTimeMillis();
ourLog.debug("Refreshed search parameter cache in {}ms", sw.getMillis());
return myActiveSearchParams.size();
} else {
return 0;
}
}
@Override
public RuntimeSearchParam getSearchParamByName(RuntimeResourceDefinition theResourceDef, String theParamName) {
Map<String, RuntimeSearchParam> params = getActiveSearchParams(theResourceDef.getName());
@ -361,48 +214,36 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
@Override
public void requestRefresh() {
synchronized (this) {
myLastRefresh = 0;
}
myResourceChangeListenerCache.requestRefresh();
}
@Override
public void forceRefresh() {
requestRefresh();
refreshCacheWithRetry();
myResourceChangeListenerCache.forceRefresh();
}
int refreshCacheWithRetry() {
Retrier<Integer> refreshCacheRetrier = new Retrier<>(() -> {
synchronized (SearchParamRegistryImpl.this) {
return mySearchParamProvider.refreshCache(this, REFRESH_INTERVAL);
}
}, MAX_RETRIES);
return refreshCacheRetrier.runWithRetry();
@Override
public ResourceChangeResult refreshCacheIfNecessary() {
return myResourceChangeListenerCache.refreshCacheIfNecessary();
}
@PostConstruct
public void scheduleJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(getClass().getName());
jobDetail.setJobClass(Job.class);
mySchedulerService.scheduleLocalJob(10 * DateUtils.MILLIS_PER_SECOND, jobDetail);
public void registerListener() {
myResourceChangeListenerCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener("SearchParameter", SearchParameterMap.newSynchronous(), this, REFRESH_INTERVAL);
}
@PreDestroy
public void unregisterListener() {
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
}
@Override
public boolean refreshCacheIfNecessary() {
if (myActiveSearchParams == null || System.currentTimeMillis() - REFRESH_INTERVAL > myLastRefresh) {
refreshCacheWithRetry();
return true;
} else {
return false;
}
}
@Override
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
public ReadOnlySearchParamCache getActiveSearchParams() {
requiresActiveSearchParams();
return Collections.unmodifiableMap(myActiveSearchParams);
if (myActiveSearchParams == null) {
throw new IllegalStateException("SearchParamRegistry has not been initialized");
}
return ReadOnlySearchParamCache.fromRuntimeSearchParamCache(myActiveSearchParams);
}
/**
@ -417,72 +258,36 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
if (myActiveSearchParams == null) {
return;
}
for (Map<String, RuntimeSearchParam> activeUniqueSearchParams : myActiveSearchParams.values()) {
for (RuntimeSearchParam searchParam : activeUniqueSearchParams.values()) {
setPhoneticEncoder(searchParam);
}
}
myActiveSearchParams.getSearchParamStream().forEach(searchParam -> myJpaSearchParamCache.setPhoneticEncoder(myPhoneticEncoder, searchParam));
}
private void setPhoneticEncoder(RuntimeSearchParam searchParam) {
if ("phonetic".equals(searchParam.getName())) {
ourLog.debug("Setting search param {} on {} phonetic encoder to {}",
searchParam.getName(), searchParam.getPath(), myPhoneticEncoder == null ? "null" : myPhoneticEncoder.name());
searchParam.setPhoneticEncoder(myPhoneticEncoder);
@Override
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
if (theResourceChangeEvent.isEmpty()) {
return;
}
ResourceChangeResult result = ResourceChangeResult.fromResourceChangeEvent(theResourceChangeEvent);
if (result.created > 0) {
ourLog.info("Adding {} search parameters to SearchParamRegistry", result.created);
}
if (result.updated > 0) {
ourLog.info("Updating {} search parameters in SearchParamRegistry", result.updated);
}
if (result.created > 0) {
ourLog.info("Deleting {} search parameters from SearchParamRegistry", result.deleted);
}
rebuildActiveSearchParams();
}
@Interceptor
public class RefreshSearchParameterCacheOnUpdate {
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED)
public void created(IBaseResource theResource) {
handle(theResource);
}
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED)
public void deleted(IBaseResource theResource) {
handle(theResource);
}
@Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED)
public void updated(IBaseResource theResource) {
handle(theResource);
}
private void handle(IBaseResource theResource) {
if (theResource != null && myFhirContext.getResourceType(theResource).equals("SearchParameter")) {
requestRefresh();
}
}
@Override
public void handleInit(Collection<IIdType> theResourceIds) {
List<IBaseResource> searchParams = theResourceIds.stream().map(id -> mySearchParamProvider.read(id)).collect(Collectors.toList());
initializeActiveSearchParams(searchParams);
}
public static class Job implements HapiJob {
@Autowired
private ISearchParamRegistry myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.refreshCacheIfNecessary();
}
}
public static Map<String, Map<String, RuntimeSearchParam>> createBuiltInSearchParamMap(FhirContext theFhirContext) {
Map<String, Map<String, RuntimeSearchParam>> resourceNameToSearchParams = new HashMap<>();
Set<String> resourceNames = theFhirContext.getResourceTypes();
for (String resourceName : resourceNames) {
RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName);
String nextResourceName = nextResDef.getName();
HashMap<String, RuntimeSearchParam> nameToParam = new HashMap<>();
resourceNameToSearchParams.put(nextResourceName, nameToParam);
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
nameToParam.put(nextSp.getName(), nextSp);
}
}
return Collections.unmodifiableMap(resourceNameToSearchParams);
@VisibleForTesting
public void resetForUnitTest() {
handleInit(Collections.emptyList());
}
}

View File

@ -28,15 +28,11 @@ import org.springframework.beans.factory.BeanCreationException;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.retry.policy.ExceptionClassifierRetryPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class Retrier<T> {
@ -63,7 +59,8 @@ public class Retrier<T> {
@Override
public boolean canRetry(RetryContext context) {
if (context.getLastThrowable() instanceof BeanCreationException) {
Throwable lastThrowable = context.getLastThrowable();
if (lastThrowable instanceof BeanCreationException || lastThrowable instanceof NullPointerException) {
return false;
}
return super.canRetry(context);
@ -76,7 +73,7 @@ public class Retrier<T> {
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
super.onError(context, callback, throwable);
if (throwable instanceof NullPointerException || throwable instanceof UnsupportedOperationException) {
if (throwable instanceof NullPointerException || throwable instanceof UnsupportedOperationException || "true".equals(System.getProperty("unit_test_mode"))) {
ourLog.error("Retry failure {}/{}: {}", context.getRetryCount(), theMaxRetries, throwable.getMessage(), throwable);
} else {
ourLog.error("Retry failure {}/{}: {}", context.getRetryCount(), theMaxRetries, throwable.toString());

View File

@ -0,0 +1,73 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
@ExtendWith(SpringExtension.class)
class ResourceChangeListenerCacheRefresherImplTest {
public static final String PATIENT_RESOURCE_NAME = "Patient";
private static final SearchParameterMap ourMap = SearchParameterMap.newSynchronous();
private static final long TEST_REFRESH_INTERVAL_MS = DateUtils.MILLIS_PER_HOUR;
@Autowired
ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
@MockBean
private ISchedulerService mySchedulerService;
@MockBean
private IResourceVersionSvc myResourceVersionSvc;
@MockBean
private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
@Configuration
@Import(RegisteredResourceListenerFactoryConfig.class)
static class SpringContext {
@Bean
IResourceChangeListenerCacheRefresher resourceChangeListenerCacheRefresher() {
return new ResourceChangeListenerCacheRefresherImpl();
}
}
@Test
public void testNotifyListenersEmptyEmptyNotInitialized() {
IResourceChangeListener listener = mock(IResourceChangeListener.class);
ResourceChangeListenerCache cache = new ResourceChangeListenerCache(PATIENT_RESOURCE_NAME, listener, ourMap, TEST_REFRESH_INTERVAL_MS);
ResourceVersionMap newResourceVersionMap = ResourceVersionMap.fromResourceTableEntities(Collections.emptyList());
assertFalse(cache.isInitialized());
myResourceChangeListenerCacheRefresher.notifyListener(cache, newResourceVersionMap);
assertTrue(cache.isInitialized());
verify(listener, times(1)).handleInit(any());
}
@Test
public void testNotifyListenersEmptyEmptyInitialized() {
IResourceChangeListener listener = mock(IResourceChangeListener.class);
ResourceChangeListenerCache cache = new ResourceChangeListenerCache(PATIENT_RESOURCE_NAME, listener, ourMap, TEST_REFRESH_INTERVAL_MS);
ResourceVersionMap newResourceVersionMap = ResourceVersionMap.fromResourceTableEntities(Collections.emptyList());
cache.setInitialized(true);
assertTrue(cache.isInitialized());
myResourceChangeListenerCacheRefresher.notifyListener(cache, newResourceVersionMap);
assertTrue(cache.isInitialized());
verifyNoInteractions(listener);
}
}

View File

@ -0,0 +1,97 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.time.Instant;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = RegisteredResourceListenerFactoryConfig.class)
class ResourceChangeListenerCacheTest {
private static final String TEST_RESOURCE_NAME = "Foo";
private static final long TEST_REFRESH_INTERVAL = DateUtils.MILLIS_PER_HOUR;
private static final IResourceChangeListener ourListener = mock(IResourceChangeListener.class);
private static final SearchParameterMap ourMap = SearchParameterMap.newSynchronous();
private static final Patient ourPatient = new Patient();
@Autowired
private ResourceChangeListenerCacheFactory myResourceChangeListenerCacheFactory;
@MockBean
ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
@MockBean
SearchParamMatcher mySearchParamMatcher;
@Test
public void doNotRefreshIfNotMatches() {
ResourceChangeListenerCache cache = myResourceChangeListenerCacheFactory.create(TEST_RESOURCE_NAME, ourMap, mock(IResourceChangeListener.class), TEST_REFRESH_INTERVAL);
cache.forceRefresh();
assertNotEquals(Instant.MIN, cache.getNextRefreshTimeForUnitTest());
// Don't reset timer if it doesn't match any searchparams
mockInMemorySupported(cache, InMemoryMatchResult.fromBoolean(false));
cache.requestRefreshIfWatching(ourPatient);
assertNotEquals(Instant.MIN, cache.getNextRefreshTimeForUnitTest());
// Reset timer if it does match searchparams
mockInMemorySupported(cache, InMemoryMatchResult.successfulMatch());
cache.requestRefreshIfWatching(ourPatient);
assertEquals(Instant.MIN, cache.getNextRefreshTimeForUnitTest());
}
private void mockInMemorySupported(ResourceChangeListenerCache thecache, InMemoryMatchResult theTheInMemoryMatchResult) {
when(mySearchParamMatcher.match(thecache.getSearchParameterMap(), ourPatient)).thenReturn(theTheInMemoryMatchResult);
}
@Test
public void testSchedule() {
ResourceChangeListenerCache cache = myResourceChangeListenerCacheFactory.create(TEST_RESOURCE_NAME, ourMap, ourListener, TEST_REFRESH_INTERVAL);
ResourceChangeListenerCache.setNowForUnitTests("08:00:00");
cache.refreshCacheIfNecessary();
verify(myResourceChangeListenerCacheRefresher, times(1)).refreshCacheAndNotifyListener(any());
reset(myResourceChangeListenerCacheRefresher);
ResourceChangeListenerCache.setNowForUnitTests("08:00:01");
cache.refreshCacheIfNecessary();
verify(myResourceChangeListenerCacheRefresher, never()).refreshCacheAndNotifyListener(any());
reset(myResourceChangeListenerCacheRefresher);
ResourceChangeListenerCache.setNowForUnitTests("08:59:59");
cache.refreshCacheIfNecessary();
verify(myResourceChangeListenerCacheRefresher, never()).refreshCacheAndNotifyListener(any());
reset(myResourceChangeListenerCacheRefresher);
ResourceChangeListenerCache.setNowForUnitTests("09:00:00");
cache.refreshCacheIfNecessary();
verify(myResourceChangeListenerCacheRefresher, never()).refreshCacheAndNotifyListener(any());
reset(myResourceChangeListenerCacheRefresher);
// Now that we passed TEST_REFRESH_INTERVAL, the cache should refresh
ResourceChangeListenerCache.setNowForUnitTests("09:00:01");
cache.refreshCacheIfNecessary();
verify(myResourceChangeListenerCacheRefresher, times(1)).refreshCacheAndNotifyListener(any());
}
}

View File

@ -0,0 +1,152 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.parser.DataFormatException;
import com.google.common.collect.Lists;
import org.apache.commons.lang3.time.DateUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
class ResourceChangeListenerRegistryImplTest {
private static final FhirContext ourFhirContext = FhirContext.forR4();
public static final String PATIENT_RESOURCE_NAME = "Patient";
public static final String OBSERVATION_RESOURCE_NAME = "Observation";
private static final long TEST_REFRESH_INTERVAL_MS = DateUtils.MILLIS_PER_HOUR;
@Autowired
ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
@Autowired
ResourceChangeListenerCacheFactory myResourceChangeListenerCacheFactory;
@MockBean
private ISchedulerService mySchedulerService;
@MockBean
private IResourceVersionSvc myResourceVersionSvc;
@MockBean
private ResourceChangeListenerCacheRefresherImpl myResourceChangeListenerCacheRefresher;
@MockBean
private InMemoryResourceMatcher myInMemoryResourceMatcher;
@MockBean
private SearchParamMatcher mySearchParamMatcher;
private final IResourceChangeListener myTestListener = mock(IResourceChangeListener.class);
private static final SearchParameterMap ourMap = SearchParameterMap.newSynchronous();
@Configuration
@Import(RegisteredResourceListenerFactoryConfig.class)
static class SpringContext {
@Bean
public IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
return new ResourceChangeListenerRegistryImpl();
}
@Bean
public FhirContext fhirContext() {
return ourFhirContext;
}
}
@BeforeEach
public void before() {
Set<IResourceChangeListenerCache> entries = new HashSet<>();
IResourceChangeListenerCache cache = myResourceChangeListenerCacheFactory.create(PATIENT_RESOURCE_NAME, ourMap, myTestListener, TEST_REFRESH_INTERVAL_MS);
entries.add(cache);
when(myInMemoryResourceMatcher.canBeEvaluatedInMemory(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
}
@Test
public void addingListenerForNonResourceFails() {
try {
myResourceChangeListenerRegistry.registerResourceResourceChangeListener("Foo", ourMap, myTestListener, TEST_REFRESH_INTERVAL_MS);
fail();
} catch (DataFormatException e) {
assertEquals("Unknown resource name \"Foo\" (this name is not known in FHIR version \"R4\")", e.getMessage());
}
}
@Test
public void addingNonInMemorySearchParamFails() {
try {
mockInMemorySupported(InMemoryMatchResult.unsupportedFromReason("TEST REASON"));
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(PATIENT_RESOURCE_NAME, ourMap, myTestListener, TEST_REFRESH_INTERVAL_MS);
fail();
} catch (IllegalArgumentException e) {
assertEquals("SearchParameterMap SearchParameterMap[] cannot be evaluated in-memory: TEST REASON. Only search parameter maps that can be evaluated in-memory may be registered.", e.getMessage());
}
}
private void mockInMemorySupported(InMemoryMatchResult theTheInMemoryMatchResult) {
when(myInMemoryResourceMatcher.canBeEvaluatedInMemory(ourMap, ourFhirContext.getResourceDefinition(PATIENT_RESOURCE_NAME))).thenReturn(theTheInMemoryMatchResult);
}
@AfterEach
public void after() {
myResourceChangeListenerRegistry.clearListenersForUnitTest();
ResourceChangeListenerCache.setNowForUnitTests(null);
}
@Test
public void registerUnregister() {
IResourceChangeListener listener1 = mock(IResourceChangeListener.class);
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(PATIENT_RESOURCE_NAME, ourMap, listener1, TEST_REFRESH_INTERVAL_MS);
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(OBSERVATION_RESOURCE_NAME, ourMap, listener1, TEST_REFRESH_INTERVAL_MS);
when(mySearchParamMatcher.match(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
assertEquals(2, myResourceChangeListenerRegistry.size());
IResourceChangeListener listener2 = mock(IResourceChangeListener.class);
myResourceChangeListenerRegistry.registerResourceResourceChangeListener(PATIENT_RESOURCE_NAME, ourMap, listener2, TEST_REFRESH_INTERVAL_MS);
assertEquals(3, myResourceChangeListenerRegistry.size());
List<ResourceChangeListenerCache> entries = Lists.newArrayList(myResourceChangeListenerRegistry.iterator());
assertThat(entries, hasSize(3));
List<IResourceChangeListener> listeners = entries.stream().map(ResourceChangeListenerCache::getResourceChangeListener).collect(Collectors.toList());
assertThat(listeners, contains(listener1, listener1, listener2));
List<String> resourceNames = entries.stream().map(IResourceChangeListenerCache::getResourceName).collect(Collectors.toList());
assertThat(resourceNames, contains(PATIENT_RESOURCE_NAME, OBSERVATION_RESOURCE_NAME, PATIENT_RESOURCE_NAME));
IResourceChangeListenerCache firstcache = entries.iterator().next();
// We made a copy
assertTrue(ourMap != firstcache.getSearchParameterMap());
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(listener1);
assertEquals(1, myResourceChangeListenerRegistry.size());
ResourceChangeListenerCache cache = myResourceChangeListenerRegistry.iterator().next();
assertEquals(PATIENT_RESOURCE_NAME, cache.getResourceName());
assertEquals(listener2, cache.getResourceChangeListener());
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(listener2);
assertEquals(0, myResourceChangeListenerRegistry.size());
}
}

View File

@ -0,0 +1,39 @@
package ca.uhn.fhir.jpa.cache;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.mockito.Mockito.verify;
@ExtendWith(SpringExtension.class)
class ResourceChangeListenerRegistryInterceptorTest {
@Autowired
ResourceChangeListenerRegistryInterceptor myResourceChangeListenerRegistryInterceptor;
@MockBean
private IInterceptorService myInterceptorBroadcaster;
@MockBean
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
@Configuration
static class SpringContext {
@Bean
public ResourceChangeListenerRegistryInterceptor resourceChangeListenerRegistryInterceptor() {
return new ResourceChangeListenerRegistryInterceptor();
}
}
@Test
public void testRefreshCalled() {
Patient patient = new Patient();
myResourceChangeListenerRegistryInterceptor.created(patient);
verify(myResourceChangeListenerRegistry).requestRefreshIfWatching(patient);
}
}

View File

@ -0,0 +1,22 @@
package ca.uhn.fhir.jpa.cache.config;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCache;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheFactory;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
@Configuration
public class RegisteredResourceListenerFactoryConfig {
@Bean
ResourceChangeListenerCacheFactory resourceChangeListenerCacheFactory() {
return new ResourceChangeListenerCacheFactory();
}
@Bean
@Scope("prototype")
ResourceChangeListenerCache resourceChangeListenerCache(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theSearchParameterMap, long theRemoteRefreshIntervalMs) {
return new ResourceChangeListenerCache(theResourceName, theResourceChangeListener, theSearchParameterMap, theRemoteRefreshIntervalMs);
}
}

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
@ -19,6 +20,7 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamUri;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.SearchParamConstants;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.util.StringUtil;
import ca.uhn.fhir.util.TestUtil;
@ -245,13 +247,13 @@ public class SearchParamExtractorDstu3Test {
}
@Override
public boolean refreshCacheIfNecessary() {
public ResourceChangeResult refreshCacheIfNecessary() {
// nothing
return false;
return new ResourceChangeResult();
}
@Override
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
public ReadOnlySearchParamCache getActiveSearchParams() {
throw new UnsupportedOperationException();
}

View File

@ -18,14 +18,17 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseEnumeration;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -40,6 +43,8 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
// TODO JA Please fix this test. Expanding FhirContext.getResourceTypes() to cover all resource types broke this test.
@Disabled
public class SearchParamExtractorMegaTest {
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamExtractorMegaTest.class);
@ -254,13 +259,13 @@ public class SearchParamExtractorMegaTest {
}
@Override
public boolean refreshCacheIfNecessary() {
public ResourceChangeResult refreshCacheIfNecessary() {
// nothing
return false;
return new ResourceChangeResult();
}
@Override
public Map<String, Map<String, RuntimeSearchParam>> getActiveSearchParams() {
public ReadOnlySearchParamCache getActiveSearchParams() {
throw new UnsupportedOperationException();
}

View File

@ -2,63 +2,151 @@ package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerCacheRefresherImpl;
import ca.uhn.fhir.jpa.cache.ResourceChangeListenerRegistryImpl;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.cache.ResourceVersionMap;
import ca.uhn.fhir.jpa.cache.config.RegisteredResourceListenerFactoryConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.SearchParameter;
import org.hl7.fhir.r4.model.StringType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
public class SearchParamRegistryImplTest {
private static final FhirContext ourFhirContext = FhirContext.forR4();
private static final ReadOnlySearchParamCache ourBuiltInSearchParams = ReadOnlySearchParamCache.fromFhirContext(ourFhirContext);
public static final int TEST_SEARCH_PARAMS = 3;
private static List<ResourceTable> ourEntities;
private static ResourceVersionMap ourResourceVersionMap;
private static int ourLastId;
private static int ourBuiltinPatientSearchParamCount;
static {
ourEntities = new ArrayList<>();
for (ourLastId = 0; ourLastId < TEST_SEARCH_PARAMS; ++ourLastId) {
ourEntities.add(createEntity(ourLastId, 1));
}
ourResourceVersionMap = ResourceVersionMap.fromResourceTableEntities(ourEntities);
ourBuiltinPatientSearchParamCount = ReadOnlySearchParamCache.fromFhirContext(ourFhirContext).getSearchParamMap("Patient").size();
}
@Autowired
SearchParamRegistryImpl mySearchParamRegistry;
@Autowired
private ResourceChangeListenerRegistryImpl myResourceChangeListenerRegistry;
@Autowired
private ResourceChangeListenerCacheRefresherImpl myChangeListenerCacheRefresher;
@MockBean
private IResourceVersionSvc myResourceVersionSvc;
@MockBean
private ISchedulerService mySchedulerService;
@MockBean
private ISearchParamProvider mySearchParamProvider;
@MockBean
private ModelConfig myModelConfig;
@MockBean
private IInterceptorService myInterceptorBroadcaster;
@MockBean
private SearchParamMatcher mySearchParamMatcher;
@MockBean
private MatchUrlService myMatchUrlService;
@Configuration
@Import(RegisteredResourceListenerFactoryConfig.class)
static class SpringConfig {
@Bean
FhirContext fhirContext() { return FhirContext.forR4(); }
FhirContext fhirContext() {
return ourFhirContext;
}
@Bean
ISearchParamRegistry searchParamRegistry() { return new SearchParamRegistryImpl(); }
ModelConfig modelConfig() {
ModelConfig modelConfig = new ModelConfig();
modelConfig.setDefaultSearchParamsCanBeOverridden(true);
return modelConfig;
}
@Bean
ISearchParamRegistry searchParamRegistry() {
return new SearchParamRegistryImpl();
}
@Bean
SearchParameterCanonicalizer searchParameterCanonicalizer(FhirContext theFhirContext) {
return new SearchParameterCanonicalizer(theFhirContext);
}
@Bean
IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
return new ResourceChangeListenerRegistryImpl();
}
@Bean
ResourceChangeListenerCacheRefresherImpl resourceChangeListenerCacheRefresher() {
return new ResourceChangeListenerCacheRefresherImpl();
}
@Bean
InMemoryResourceMatcher inMemoryResourceMatcher() {
InMemoryResourceMatcher retval = mock(InMemoryResourceMatcher.class);
when(retval.canBeEvaluatedInMemory(any(), any())).thenReturn(InMemoryMatchResult.successfulMatch());
return retval;
}
}
@Nonnull
private static ResourceTable createEntity(long theId, int theVersion) {
ResourceTable searchParamEntity = new ResourceTable();
searchParamEntity.setResourceType("SearchParameter");
searchParamEntity.setId(theId);
searchParamEntity.setVersion(theVersion);
return searchParamEntity;
}
private int myAnswerCount = 0;
@ -66,81 +154,152 @@ public class SearchParamRegistryImplTest {
@BeforeEach
public void before() {
myAnswerCount = 0;
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(ourResourceVersionMap);
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
// Our first refresh adds our test searchparams to the registry
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), TEST_SEARCH_PARAMS, 0, 0);
assertEquals(TEST_SEARCH_PARAMS, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertDbCalled();
assertEquals(ourBuiltInSearchParams.size(), mySearchParamRegistry.getActiveSearchParams().size());
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
}
@AfterEach
public void after() {
myResourceChangeListenerRegistry.clearCachesForUnitTest();
// Empty out the searchparam registry
mySearchParamRegistry.resetForUnitTest();
}
@Test
public void testRefreshAfterExpiry() {
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
mySearchParamRegistry.requestRefresh();
assertEquals(146, mySearchParamRegistry.doRefresh(100000));
// Second time we don't need to run because we ran recently
assertEquals(0, mySearchParamRegistry.doRefresh(100000));
assertEquals(146, mySearchParamRegistry.getActiveSearchParams().size());
assertEmptyResult(mySearchParamRegistry.refreshCacheIfNecessary());
}
@Test
public void testRefreshCacheIfNecessary() {
// Second refresh does not call the database
assertEmptyResult(mySearchParamRegistry.refreshCacheIfNecessary());
assertEquals(TEST_SEARCH_PARAMS, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertDbNotCalled();
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
mySearchParamRegistry.doRefresh(t.getArgument(1, Long.class));
return 0;
});
// Requesting a refresh calls the database and adds nothing
mySearchParamRegistry.requestRefresh();
assertEmptyResult(mySearchParamRegistry.refreshCacheIfNecessary());
assertEquals(TEST_SEARCH_PARAMS, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertDbCalled();
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
assertTrue(mySearchParamRegistry.refreshCacheIfNecessary());
assertFalse(mySearchParamRegistry.refreshCacheIfNecessary());
// Requesting a refresh after adding a new search parameter calls the database and adds one
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParamRegistry.requestRefresh();
assertTrue(mySearchParamRegistry.refreshCacheIfNecessary());
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 0);
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertDbCalled();
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
// Requesting a refresh after adding a new search parameter calls the database and
// removes the one added above and adds this new one
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParamRegistry.requestRefresh();
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 1);
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertDbCalled();
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
// Requesting a refresh after adding a new search parameter calls the database,
// removes the ACTIVE one and adds the new one because this is a mock test
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.DRAFT);
mySearchParamRegistry.requestRefresh();
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 1);
assertDbCalled();
// the new one does not appear in our patient search params because it's DRAFT
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount);
}
@Test
public void testSearchParamUpdate() {
// Requesting a refresh after adding a new search parameter calls the database and adds one
List<ResourceTable> newEntities = resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParamRegistry.requestRefresh();
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 1, 0, 0);
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertDbCalled();
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
// Update the resource without changing anything that would affect our cache
ResourceTable lastEntity = newEntities.get(newEntities.size() - 1);
lastEntity.setVersion(2);
resetMock(Enumerations.PublicationStatus.ACTIVE, newEntities);
mySearchParamRegistry.requestRefresh();
assertResult(mySearchParamRegistry.refreshCacheIfNecessary(), 0, 1, 0);
assertEquals(TEST_SEARCH_PARAMS + 1, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
assertDbCalled();
assertPatientSearchParamSize(ourBuiltinPatientSearchParamCount + 1);
}
private void assertPatientSearchParamSize(int theExpectedSize) {
assertEquals(theExpectedSize, mySearchParamRegistry.getActiveSearchParams("Patient").size());
}
private void assertResult(ResourceChangeResult theResult, long theExpectedAdded, long theExpectedUpdated, long theExpectedRemoved) {
assertEquals(theExpectedAdded, theResult.created, "added results");
assertEquals(theExpectedUpdated, theResult.updated, "updated results");
assertEquals(theExpectedRemoved, theResult.deleted, "removed results");
}
private void assertEmptyResult(ResourceChangeResult theResult) {
assertResult(theResult, 0, 0, 0);
}
private void assertDbCalled() {
verify(myResourceVersionSvc, times(1)).getVersionMap(anyString(), any());
reset(myResourceVersionSvc);
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(ourResourceVersionMap);
}
private void assertDbNotCalled() {
verify(myResourceVersionSvc, never()).getVersionMap(anyString(), any());
reset(myResourceVersionSvc);
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(ourResourceVersionMap);
}
@Test
public void testGetActiveUniqueSearchParams_Empty() {
assertThat(mySearchParamRegistry.getActiveUniqueSearchParams("Patient"), Matchers.empty());
assertThat(mySearchParamRegistry.getActiveUniqueSearchParams("Patient"), is(empty()));
}
@Test
public void testGetActiveSearchParams() {
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider());
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
public void testGetActiveSearchParamsRetries() {
AtomicBoolean retried = new AtomicBoolean(false);
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenAnswer(t -> {
if (myAnswerCount == 0) {
myAnswerCount++;
retried.set(true);
throw new InternalErrorException("this is an error!");
}
mySearchParamRegistry.doRefresh(0);
return 0;
return ourResourceVersionMap;
});
Map<String, RuntimeSearchParam> outcome = mySearchParamRegistry.getActiveSearchParams("Patient");
assertFalse(retried.get());
mySearchParamRegistry.forceRefresh();
Map<String, RuntimeSearchParam> activeSearchParams = mySearchParamRegistry.getActiveSearchParams("Patient");
assertTrue(retried.get());
assertEquals(ourBuiltInSearchParams.getSearchParamMap("Patient").size(), activeSearchParams.size());
}
@Test
public void testExtractExtensions() {
SearchParameter searchParameter = new SearchParameter();
searchParameter.setCode("foo");
searchParameter.setStatus(Enumerations.PublicationStatus.ACTIVE);
searchParameter.setType(Enumerations.SearchParamType.TOKEN);
searchParameter.setExpression("Patient.name");
searchParameter.addBase("Patient");
searchParameter.addExtension("http://foo", new StringType("FOO"));
searchParameter.addExtension("http://bar", new StringType("BAR"));
public void testAddActiveSearchparam() {
// Initialize the registry
mySearchParamRegistry.forceRefresh();
// Invalid entries
searchParameter.addExtension("http://bar", null);
searchParameter.addExtension(null, new StringType("BAR"));
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider(searchParameter));
when(mySearchParamProvider.refreshCache(any(), anyLong())).thenAnswer(t -> {
mySearchParamRegistry.doRefresh(0);
return 0;
});
resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus.ACTIVE);
mySearchParamRegistry.forceRefresh();
Map<String, RuntimeSearchParam> outcome = mySearchParamRegistry.getActiveSearchParams("Patient");
@ -151,7 +310,39 @@ public class SearchParamRegistryImplTest {
assertEquals(1, converted.getExtensions("http://foo").size());
IPrimitiveType<?> value = (IPrimitiveType<?>) converted.getExtensions("http://foo").get(0).getValue();
assertEquals("FOO", value.getValueAsString());
}
private List<ResourceTable> resetDatabaseToOrigSearchParamsPlusNewOneWithStatus(Enumerations.PublicationStatus theStatus) {
// Add a new search parameter entity
List<ResourceTable> newEntities = new ArrayList(ourEntities);
newEntities.add(createEntity(++ourLastId, 1));
resetMock(theStatus, newEntities);
return newEntities;
}
private void resetMock(Enumerations.PublicationStatus theStatus, List<ResourceTable> theNewEntities) {
ResourceVersionMap resourceVersionMap = ResourceVersionMap.fromResourceTableEntities(theNewEntities);
when(myResourceVersionSvc.getVersionMap(anyString(), any())).thenReturn(resourceVersionMap);
// When we ask for the new entity, return our foo search parameter
when(mySearchParamProvider.search(any())).thenReturn(new SimpleBundleProvider(buildSearchParameter(theStatus)));
}
@Nonnull
private SearchParameter buildSearchParameter(Enumerations.PublicationStatus theStatus) {
SearchParameter searchParameter = new SearchParameter();
searchParameter.setCode("foo");
searchParameter.setStatus(theStatus);
searchParameter.setType(Enumerations.SearchParamType.TOKEN);
searchParameter.setExpression("Patient.name");
searchParameter.addBase("Patient");
searchParameter.addExtension("http://foo", new StringType("FOO"));
searchParameter.addExtension("http://bar", new StringType("BAR"));
// Invalid entries
searchParameter.addExtension("http://bar", null);
searchParameter.addExtension(null, new StringType("BAR"));
return searchParameter;
}
}

View File

@ -37,7 +37,7 @@ public class SubscriptionStrategyEvaluator {
}
public SubscriptionMatchingStrategy determineStrategy(String theCriteria) {
InMemoryMatchResult result = myInMemoryResourceMatcher.match(theCriteria, null, null);
InMemoryMatchResult result = myInMemoryResourceMatcher.canBeEvaluatedInMemory(theCriteria);
if (result.supported()) {
return SubscriptionMatchingStrategy.IN_MEMORY;
}

View File

@ -21,9 +21,12 @@ package ca.uhn.fhir.jpa.subscription.match.registry;
*/
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.model.sched.HapiJob;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.cache.IResourceChangeEvent;
import ca.uhn.fhir.jpa.cache.IResourceChangeListener;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCache;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
@ -34,22 +37,28 @@ import ca.uhn.fhir.rest.param.TokenParam;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Subscription;
import org.quartz.JobExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.stream.Collectors;
public class SubscriptionLoader {
public class SubscriptionLoader implements IResourceChangeListener {
private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionLoader.class);
private static final int MAX_RETRIES = 60; // 60 * 5 seconds = 5 minutes
private static long REFRESH_INTERVAL = DateUtils.MILLIS_PER_MINUTE;
private final Object mySyncSubscriptionsLock = new Object();
@Autowired
private SubscriptionRegistry mySubscriptionRegistry;
@ -62,6 +71,10 @@ public class SubscriptionLoader {
private SubscriptionActivatingSubscriber mySubscriptionActivatingInterceptor;
@Autowired
private ISearchParamRegistry mySearchParamRegistry;
@Autowired
private IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
private SearchParameterMap mySearchParameterMap;
/**
* Constructor
@ -70,11 +83,27 @@ public class SubscriptionLoader {
super();
}
@PostConstruct
public void registerListener() {
mySearchParameterMap = getSearchParameterMap();
IResourceChangeListenerCache subscriptionCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener("Subscription", mySearchParameterMap, this, REFRESH_INTERVAL);
subscriptionCache.forceRefresh();
}
@PreDestroy
public void unregisterListener() {
myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this);
}
private boolean subscriptionsDaoExists() {
return myDaoRegistry != null && myDaoRegistry.isResourceTypeSupported("Subscription");
}
/**
* Read the existing subscriptions from the database
*/
public void syncSubscriptions() {
if (myDaoRegistry != null && !myDaoRegistry.isResourceTypeSupported("Subscription")) {
if (!subscriptionsDaoExists()) {
return;
}
if (!mySyncSubscriptionsSemaphore.tryAcquire()) {
@ -87,16 +116,6 @@ public class SubscriptionLoader {
}
}
@PostConstruct
public void scheduleJob() {
ScheduledJobDefinition jobDetail = new ScheduledJobDefinition();
jobDetail.setId(getClass().getName());
jobDetail.setJobClass(Job.class);
mySchedulerService.scheduleLocalJob(DateUtils.MILLIS_PER_MINUTE, jobDetail);
syncSubscriptions();
}
@VisibleForTesting
public void acquireSemaphoreForUnitTest() throws InterruptedException {
mySyncSubscriptionsSemaphore.acquire();
@ -122,16 +141,8 @@ public class SubscriptionLoader {
synchronized (mySyncSubscriptionsLock) {
ourLog.debug("Starting sync subscriptions");
SearchParameterMap map = new SearchParameterMap();
if (mySearchParamRegistry.getActiveSearchParam("Subscription", "status") != null) {
map.add(Subscription.SP_STATUS, new TokenOrListParam()
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
}
map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS);
IBundleProvider subscriptionBundleList = myDaoRegistry.getSubscriptionDao().search(map);
IBundleProvider subscriptionBundleList = getSubscriptionDao().search(mySearchParameterMap);
Integer subscriptionCount = subscriptionBundleList.size();
assert subscriptionCount != null;
@ -141,41 +152,68 @@ public class SubscriptionLoader {
List<IBaseResource> resourceList = subscriptionBundleList.getResources(0, subscriptionCount);
Set<String> allIds = new HashSet<>();
int activatedCount = 0;
int registeredCount = 0;
return updateSubscriptionRegistry(resourceList);
}
}
for (IBaseResource resource : resourceList) {
String nextId = resource.getIdElement().getIdPart();
allIds.add(nextId);
private IFhirResourceDao<?> getSubscriptionDao() {
return myDaoRegistry.getSubscriptionDao();
}
boolean activated = mySubscriptionActivatingInterceptor.activateSubscriptionIfRequired(resource);
if (activated) {
activatedCount++;
}
@Nonnull
private SearchParameterMap getSearchParameterMap() {
SearchParameterMap map = new SearchParameterMap();
boolean registered = mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(resource);
if (registered) {
registeredCount++;
}
if (mySearchParamRegistry.getActiveSearchParam("Subscription", "status") != null) {
map.add(Subscription.SP_STATUS, new TokenOrListParam()
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode()))
.addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode())));
}
map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS);
return map;
}
private int updateSubscriptionRegistry(List<IBaseResource> theResourceList) {
Set<String> allIds = new HashSet<>();
int activatedCount = 0;
int registeredCount = 0;
for (IBaseResource resource : theResourceList) {
String nextId = resource.getIdElement().getIdPart();
allIds.add(nextId);
boolean activated = mySubscriptionActivatingInterceptor.activateSubscriptionIfRequired(resource);
if (activated) {
activatedCount++;
}
mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds);
ourLog.debug("Finished sync subscriptions - activated {} and registered {}", resourceList.size(), registeredCount);
return activatedCount;
boolean registered = mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(resource);
if (registered) {
registeredCount++;
}
}
mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds);
ourLog.debug("Finished sync subscriptions - activated {} and registered {}", theResourceList.size(), registeredCount);
return activatedCount;
}
public static class Job implements HapiJob {
@Autowired
private SubscriptionLoader myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.syncSubscriptions();
@Override
public void handleInit(Collection<IIdType> theResourceIds) {
if (!subscriptionsDaoExists()) {
ourLog.warn("Subsriptions are enabled on this server, but there is no Subscription DAO configured.");
return;
}
IFhirResourceDao<?> subscriptionDao = getSubscriptionDao();
List<IBaseResource> resourceList = theResourceIds.stream().map(subscriptionDao::read).collect(Collectors.toList());
updateSubscriptionRegistry(resourceList);
}
@Override
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
// For now ignore the contents of theResourceChangeEvent. In the future, consider updating the registry based on
// known subscriptions that have been created, updated & deleted
syncSubscriptions();
}
}

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
@ -24,6 +25,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.PlatformTransactionManager;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
@ -76,6 +79,11 @@ public class DaoSubscriptionMatcherTest {
return FhirContext.forR4();
}
@Bean
public IResourceVersionSvc resourceVersionSvc() {
return mock(IResourceVersionSvc.class, RETURNS_DEEP_STUBS);
}
}
}

View File

@ -4,15 +4,16 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.interceptor.executor.InterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistry;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannelFactory;
import ca.uhn.fhir.jpa.subscription.channel.subscription.IChannelNamer;
import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory;
import ca.uhn.fhir.jpa.subscription.match.config.SubscriptionProcessorConfig;
import ca.uhn.fhir.jpa.subscription.module.config.MockFhirClientSearchParamProvider;
import ca.uhn.fhir.jpa.subscription.module.config.TestSubscriptionConfig;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.model.primitive.IdDt;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -20,32 +21,42 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import java.util.Collections;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
SearchParamConfig.class,
SubscriptionProcessorConfig.class,
TestSubscriptionConfig.class,
BaseSubscriptionTest.MyConfig.class
})
public abstract class BaseSubscriptionTest {
static {
System.setProperty("unit_test_mode", "true");
}
@Autowired
protected IInterceptorService myInterceptorRegistry;
@Autowired
ISearchParamRegistry mySearchParamRegistry;
SearchParamRegistryImpl mySearchParamRegistry;
@Autowired
MockFhirClientSearchParamProvider myMockFhirClientSearchParamProvider;
@BeforeEach
public void before() {
mySearchParamRegistry.handleInit(Collections.emptyList());
}
@AfterEach
public void afterClearAnonymousLambdas() {
myInterceptorRegistry.unregisterAllInterceptors();
}
public void initSearchParamRegistry(IBundleProvider theBundleProvider) {
myMockFhirClientSearchParamProvider.setBundleProvider(theBundleProvider);
mySearchParamRegistry.forceRefresh();
public void initSearchParamRegistry(IBaseResource theReadResource) {
myMockFhirClientSearchParamProvider.setReadResource(theReadResource);
mySearchParamRegistry.handleInit(Collections.singletonList(new IdDt()));
}
@Configuration

View File

@ -4,6 +4,8 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.springframework.beans.factory.annotation.Autowired;
public class MockFhirClientSearchParamProvider implements ISearchParamProvider {
@ -18,6 +20,8 @@ public class MockFhirClientSearchParamProvider implements ISearchParamProvider {
public void setBundleProvider(IBundleProvider theBundleProvider) { myMockProvider.setBundleProvider(theBundleProvider); }
public void setReadResource(IBaseResource theReadResource) { myMockProvider.setReadResource(theReadResource);}
public void setFailCount(int theFailCount) { myMockProvider.setFailCount(theFailCount); }
public int getFailCount() { return myMockProvider.getFailCount(); }
@ -26,8 +30,7 @@ public class MockFhirClientSearchParamProvider implements ISearchParamProvider {
public IBundleProvider search(SearchParameterMap theParams) { return myMockProvider.search(theParams); }
@Override
public int refreshCache(SearchParamRegistryImpl theSearchParamRegistry, long theRefreshInterval) {
mySearchParamRegistry.doRefresh(0);
return 0;
public IBaseResource read(IIdType theId) {
return myMockProvider.read(theId);
}
}

View File

@ -3,14 +3,20 @@ package ca.uhn.fhir.jpa.subscription.module.config;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
public class MockProvider {
private IBundleProvider myBundleProvider = new SimpleBundleProvider();
private int myFailCount = 0;
private IBaseResource myReadResource;
public void setBundleProvider(IBundleProvider theBundleProvider) {
myBundleProvider = theBundleProvider;
}
public void setReadResource(IBaseResource theReadResource) {
myReadResource = theReadResource;
}
public IBundleProvider search(SearchParameterMap theParams) {
if (myFailCount > 0) {
@ -28,4 +34,7 @@ public class MockProvider {
return myFailCount;
}
public IBaseResource read(IIdType theId) {
return myReadResource;
}
}

View File

@ -1,15 +1,19 @@
package ca.uhn.fhir.jpa.subscription.module.config;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
import ca.uhn.fhir.jpa.cache.ResourceVersionMap;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.InMemorySubscriptionMatcher;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestPropertySource;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Configuration
@TestPropertySource(properties = {
"scheduling_disabled=true"
@ -27,13 +31,19 @@ public class TestSubscriptionConfig {
}
@Bean
public IGenericClient fhirClient(FhirContext theFhirContext) {
return Mockito.mock(IGenericClient.class);
};
public IGenericClient fhirClient() {
return mock(IGenericClient.class);
}
@Bean
public InMemorySubscriptionMatcher inMemorySubscriptionMatcher() {
return new InMemorySubscriptionMatcher();
}
@Bean
public IResourceVersionSvc resourceVersionSvc() {
IResourceVersionSvc retval = mock(IResourceVersionSvc.class);
when(retval.getVersionMap(any(), any())).thenReturn(ResourceVersionMap.empty());
return retval;
}
}

View File

@ -5,6 +5,8 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@ -15,15 +17,18 @@ import static org.mockito.Mockito.mock;
@Configuration
@Import(TestSubscriptionConfig.class)
public class TestSubscriptionDstu3Config {
private static final Logger ourLog = LoggerFactory.getLogger(TestSubscriptionDstu3Config.class);
private static final FhirContext ourFhirContext = FhirContext.forDstu3();
@Bean
public FhirContext fhirContext() {
return FhirContext.forDstu3();
return ourFhirContext;
}
@Bean
public IValidationSupport validationSupport() {
return FhirContext.forDstu3().getValidationSupport();
public IValidationSupport validationSupport(FhirContext theFhirContext) {
return theFhirContext.getValidationSupport();
}
@Bean

View File

@ -1,14 +1,11 @@
package ca.uhn.fhir.jpa.subscription.module.matcher;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionMatchingStrategy;
import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator;
import ca.uhn.fhir.jpa.subscription.module.BaseSubscriptionDstu3Test;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.util.UrlUtil;
import org.hl7.fhir.dstu3.model.BodySite;
import org.hl7.fhir.dstu3.model.CodeableConcept;
@ -37,7 +34,6 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -52,8 +48,6 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
SearchParamMatcher mySearchParamMatcher;
@Autowired
ModelConfig myModelConfig;
@Autowired
FhirContext myFhirContext;
private void assertUnsupported(IBaseResource resource, String criteria) {
assertFalse(mySearchParamMatcher.match(criteria, resource, null).supported());
@ -372,8 +366,7 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid");
initSearchParamRegistry(bundle);
initSearchParamRegistry(sp);
{
Provenance prov = new Provenance();
@ -404,8 +397,7 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid");
initSearchParamRegistry(bundle);
initSearchParamRegistry(sp);
{
BodySite bodySite = new BodySite();
@ -496,8 +488,7 @@ public class InMemorySubscriptionMatcherR3Test extends BaseSubscriptionDstu3Test
sp.setXpathUsage(SearchParameter.XPathUsageType.NORMAL);
sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
IBundleProvider bundle = new SimpleBundleProvider(Collections.singletonList(sp), "uuid");
initSearchParamRegistry(bundle);
initSearchParamRegistry(sp);
{
ProcedureRequest pr = new ProcedureRequest();

View File

@ -4,6 +4,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
@ -35,6 +36,7 @@ import org.springframework.transaction.PlatformTransactionManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -69,6 +71,8 @@ public class WebsocketConnectionValidatorTest {
@Autowired
WebsocketConnectionValidator myWebsocketConnectionValidator;
@Autowired
IResourceChangeListenerRegistry myResourceChangeListenerRegistry;
@BeforeEach
public void before() {
@ -141,6 +145,10 @@ public class WebsocketConnectionValidatorTest {
return new WebsocketConnectionValidator();
}
@Bean
public IResourceChangeListenerRegistry resourceChangeListenerRegistry() {
return mock(IResourceChangeListenerRegistry.class, RETURNS_DEEP_STUBS);
}
}
}

View File

@ -5,6 +5,7 @@ import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.cache.IResourceVersionSvc;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
@ -50,6 +51,8 @@ public class SubscriptionSubmitInterceptorLoaderTest {
private SubscriptionSubmitInterceptorLoader mySubscriptionSubmitInterceptorLoader;
@Autowired
private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor;
@MockBean
private IResourceVersionSvc myResourceVersionSvc;
/**
* It should be possible to run only the {@link SubscriptionSubmitterConfig} without the