Force a flush of the SearchParameter cache sooner when SPs change (#2030)

* Force a flush of the SearchParameter cache sooner when SPs change

* Add changelog

* Test fixes
This commit is contained in:
James Agnew 2020-08-09 19:30:59 -04:00 committed by GitHub
parent 096f310cae
commit e61b39fc30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 197 additions and 61 deletions

View File

@ -56,7 +56,7 @@ public class NumberParam extends BaseParamWithPrefix<NumberParam> implements IQu
* Constructor * Constructor
* *
* @param theValue * @param theValue
* A string value, e.g. "&gt;5.0" * A string value, e.g. "gt5.0"
*/ */
public NumberParam(String theValue) { public NumberParam(String theValue) {
setValueAsQueryToken(null, null, null, theValue); setValueAsQueryToken(null, null, null, theValue);

View File

@ -0,0 +1,7 @@
---
type: add
issue: 2023
title: "The JPA server maintains a cache of active SearchParameeter resources that can cause misleading results
if a SearchParameter is changed and other resources that would be indexed by the changed SearchParameter are updated
before the cache refreshes. A new interceptor has been added that should force a refresh sooner, especially on
non-clustered systems."

View File

@ -51,6 +51,7 @@ public class FhirResourceDaoSearchParameterDstu2 extends BaseHapiFhirResourceDao
protected void postPersist(ResourceTable theEntity, SearchParameter theResource) { protected void postPersist(ResourceTable theEntity, SearchParameter theResource) {
super.postPersist(theEntity, theResource); super.postPersist(theEntity, theResource);
markAffectedResources(theResource); markAffectedResources(theResource);
} }
@Override @Override

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.dstu2; package ca.uhn.fhir.jpa.dao.dstu2;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
@ -28,6 +29,8 @@ import ca.uhn.fhir.model.primitive.DecimalDt;
import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.IntegerDt;
import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider; 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.DateParam;
import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.ReferenceParam;
@ -232,6 +235,47 @@ public class FhirResourceDaoDstu2SearchCustomSearchParamTest extends BaseJpaDstu
} }
/**
* See #2023
*/
@Test
public void testNumberSearchParam() {
SearchParameter numberParameter = new ca.uhn.fhir.model.dstu2.resource.SearchParameter();
numberParameter.setId("future-appointment-count");
numberParameter.setName("Future Appointment Count");
numberParameter.setCode("future-appointment-count");
numberParameter.setDescription("Count of future appointments for the patient");
numberParameter.setUrl("http://integer");
numberParameter.setStatus(ca.uhn.fhir.model.dstu2.valueset.ConformanceResourceStatusEnum.ACTIVE);
numberParameter.setBase(ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum.PATIENT);
numberParameter.setType(ca.uhn.fhir.model.dstu2.valueset.SearchParamTypeEnum.NUMBER);
numberParameter.setXpathUsage(XPathUsageTypeEnum.NORMAL);
numberParameter.setXpath("Patient.extension('http://integer')");
mySearchParameterDao.update(numberParameter);
// This fires every 10 seconds
mySearchParamRegistry.refreshCacheIfNecessary();
Patient patient = new Patient();
patient.setId("future-appointment-count-pt");
patient.setActive(true);
patient.addUndeclaredExtension(false, "http://integer", new IntegerDt(1));
myPatientDao.update(patient);
IBundleProvider search;
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam(1)));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("gt0")));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("lt0")));
assertEquals(0, search.size());
}
@Test @Test
public void testIncludeExtensionReferenceAsRecurse() { public void testIncludeExtensionReferenceAsRecurse() {
SearchParameter attendingSp = new SearchParameter(); SearchParameter attendingSp = new SearchParameter();

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.jpa.dao.r4; package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor;
import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.api.Pointcut;
@ -9,7 +10,11 @@ import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage; import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.model.api.Include; 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.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.DateParam;
import ca.uhn.fhir.rest.param.NumberParam; import ca.uhn.fhir.rest.param.NumberParam;
import ca.uhn.fhir.rest.param.ReferenceOrListParam; import ca.uhn.fhir.rest.param.ReferenceOrListParam;
@ -112,6 +117,46 @@ public class FhirResourceDaoR4SearchCustomSearchParamTest extends BaseJpaR4Test
mySearchParamRegistry.forceRefresh(); mySearchParamRegistry.forceRefresh();
} }
/**
* See #2023
*/
@Test
public void testNumberSearchParam() {
SearchParameter numberParameter = new SearchParameter();
numberParameter.setId("future-appointment-count");
numberParameter.setName("Future Appointment Count");
numberParameter.setCode("future-appointment-count");
numberParameter.setDescription("Count of future appointments for the patient");
numberParameter.setUrl("http://integer");
numberParameter.setStatus(Enumerations.PublicationStatus.ACTIVE);
numberParameter.addBase("Patient");
numberParameter.setType(Enumerations.SearchParamType.NUMBER);
numberParameter.setExpression("Patient.extension('http://integer')");
mySearchParameterDao.update(numberParameter);
// This fires every 10 seconds
mySearchParamRegistry.refreshCacheIfNecessary();
Patient patient = new Patient();
patient.setId("future-appointment-count-pt");
patient.setActive(true);
patient.addExtension( "http://integer", new IntegerType(1));
myPatientDao.update(patient);
IBundleProvider search;
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam(1)));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("gt0")));
assertEquals(1, search.size());
search = myPatientDao.search(SearchParameterMap.newSynchronous("future-appointment-count", new NumberParam("lt0")));
assertEquals(0, search.size());
}
/** /**
* Draft search parameters should be ok even if they aren't completely valid * Draft search parameters should be ok even if they aren't completely valid
*/ */

View File

@ -24,8 +24,10 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; 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.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 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.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.HapiJob; import ca.uhn.fhir.jpa.model.sched.HapiJob;
@ -38,8 +40,6 @@ import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.DatatypeUtil;
import ca.uhn.fhir.util.HapiExtensions;
import ca.uhn.fhir.util.SearchParameterUtil; import ca.uhn.fhir.util.SearchParameterUtil;
import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.StopWatch;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -51,6 +51,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -89,7 +90,8 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
private volatile long myLastRefresh; private volatile long myLastRefresh;
@Autowired @Autowired
private IInterceptorBroadcaster myInterceptorBroadcaster; private IInterceptorService myInterceptorBroadcaster;
private RefreshSearchParameterCacheOnUpdate myInterceptor;
@Override @Override
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
@ -236,8 +238,16 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
} }
@PostConstruct @PostConstruct
public void postConstruct() { public void start() {
myBuiltInSearchParams = createBuiltInSearchParamMap(myFhirContext); myBuiltInSearchParams = createBuiltInSearchParamMap(myFhirContext);
myInterceptor = new RefreshSearchParameterCacheOnUpdate();
myInterceptorBroadcaster.registerInterceptor(myInterceptor);
}
@PreDestroy
public void stop() {
myInterceptorBroadcaster.unregisterInterceptor(myInterceptor);
} }
public int doRefresh(long theRefreshInterval) { public int doRefresh(long theRefreshInterval) {
@ -376,16 +386,6 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
mySchedulerService.scheduleLocalJob(10 * DateUtils.MILLIS_PER_SECOND, jobDetail); mySchedulerService.scheduleLocalJob(10 * DateUtils.MILLIS_PER_SECOND, jobDetail);
} }
public static class Job implements HapiJob {
@Autowired
private ISearchParamRegistry myTarget;
@Override
public void execute(JobExecutionContext theContext) {
myTarget.refreshCacheIfNecessary();
}
}
@Override @Override
public boolean refreshCacheIfNecessary() { public boolean refreshCacheIfNecessary() {
if (myActiveSearchParams == null || System.currentTimeMillis() - REFRESH_INTERVAL > myLastRefresh) { if (myActiveSearchParams == null || System.currentTimeMillis() - REFRESH_INTERVAL > myLastRefresh) {
@ -402,30 +402,12 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
return Collections.unmodifiableMap(myActiveSearchParams); return Collections.unmodifiableMap(myActiveSearchParams);
} }
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);
}
/** /**
* All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder. * All SearchParameters with the name "phonetic" encode the normalized index value using this phonetic encoder.
* *
* @since 5.1.0 * @since 5.1.0
*/ */
@Override
public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) { public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
myPhoneticEncoder = thePhoneticEncoder; myPhoneticEncoder = thePhoneticEncoder;
@ -446,4 +428,58 @@ public class SearchParamRegistryImpl implements ISearchParamRegistry {
searchParam.setPhoneticEncoder(myPhoneticEncoder); searchParam.setPhoneticEncoder(myPhoneticEncoder);
} }
} }
@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();
}
}
}
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);
}
} }

View File

@ -96,31 +96,33 @@ public class SearchParameterCanonicalizer {
String path = theNextSp.getXpath(); String path = theNextSp.getXpath();
RestSearchParameterTypeEnum paramType = null; RestSearchParameterTypeEnum paramType = null;
RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null;
switch (theNextSp.getTypeElement().getValueAsEnum()) { if (theNextSp.getTypeElement().getValueAsEnum() != null) {
case COMPOSITE: switch (theNextSp.getTypeElement().getValueAsEnum()) {
paramType = RestSearchParameterTypeEnum.COMPOSITE; case COMPOSITE:
break; paramType = RestSearchParameterTypeEnum.COMPOSITE;
case DATE_DATETIME: break;
paramType = RestSearchParameterTypeEnum.DATE; case DATE_DATETIME:
break; paramType = RestSearchParameterTypeEnum.DATE;
case NUMBER: break;
paramType = RestSearchParameterTypeEnum.NUMBER; case NUMBER:
break; paramType = RestSearchParameterTypeEnum.NUMBER;
case QUANTITY: break;
paramType = RestSearchParameterTypeEnum.QUANTITY; case QUANTITY:
break; paramType = RestSearchParameterTypeEnum.QUANTITY;
case REFERENCE: break;
paramType = RestSearchParameterTypeEnum.REFERENCE; case REFERENCE:
break; paramType = RestSearchParameterTypeEnum.REFERENCE;
case STRING: break;
paramType = RestSearchParameterTypeEnum.STRING; case STRING:
break; paramType = RestSearchParameterTypeEnum.STRING;
case TOKEN: break;
paramType = RestSearchParameterTypeEnum.TOKEN; case TOKEN:
break; paramType = RestSearchParameterTypeEnum.TOKEN;
case URI: break;
paramType = RestSearchParameterTypeEnum.URI; case URI:
break; paramType = RestSearchParameterTypeEnum.URI;
break;
}
} }
if (theNextSp.getStatus() != null) { if (theNextSp.getStatus() != null) {
switch (theNextSp.getStatusElement().getValueAsEnum()) { switch (theNextSp.getStatusElement().getValueAsEnum()) {

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.searchparam.registry;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
import ca.uhn.fhir.interceptor.api.IInterceptorService;
import ca.uhn.fhir.jpa.model.entity.ModelConfig; import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.SimpleBundleProvider;
@ -46,7 +47,7 @@ public class SearchParamRegistryImplTest {
@MockBean @MockBean
private ModelConfig myModelConfig; private ModelConfig myModelConfig;
@MockBean @MockBean
private IInterceptorBroadcaster myInterceptorBroadcaster; private IInterceptorService myInterceptorBroadcaster;
@Configuration @Configuration
static class SpringConfig { static class SpringConfig {