merge fix for issue with infinite cache refresh loop
This commit is contained in:
parent
361f6d2983
commit
bc47cb70f1
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
type: fix
|
||||||
|
issue: 6044
|
||||||
|
backport: 7.2.2
|
||||||
|
title: "Fixed an issue where doing a cache refresh with advanced Hibernate Search
|
||||||
|
enabled would result in an infinite loop of cache refresh -> search for
|
||||||
|
StructureDefinition -> cache refresh, etc
|
||||||
|
"
|
|
@ -151,6 +151,15 @@ public class FulltextSearchSvcImpl implements IFulltextSearchSvc {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the registry has not been initialized
|
||||||
|
// we cannot use HibernateSearch because it
|
||||||
|
// will, internally, trigger a new search
|
||||||
|
// when it refreshes the search parameters
|
||||||
|
// (which will cause an infinite loop)
|
||||||
|
if (!mySearchParamRegistry.isInitialized()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return myStorageSettings.isAdvancedHSearchIndexing()
|
return myStorageSettings.isAdvancedHSearchIndexing()
|
||||||
&& myAdvancedIndexQueryBuilder.canUseHibernateSearch(theResourceType, myParams, mySearchParamRegistry);
|
&& myAdvancedIndexQueryBuilder.canUseHibernateSearch(theResourceType, myParams, mySearchParamRegistry);
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,7 @@ public class ExtendedHSearchSearchBuilder {
|
||||||
public boolean canUseHibernateSearch(
|
public boolean canUseHibernateSearch(
|
||||||
String theResourceType, SearchParameterMap myParams, ISearchParamRegistry theSearchParamRegistry) {
|
String theResourceType, SearchParameterMap myParams, ISearchParamRegistry theSearchParamRegistry) {
|
||||||
boolean canUseHibernate = true;
|
boolean canUseHibernate = true;
|
||||||
|
|
||||||
ResourceSearchParams resourceActiveSearchParams = theSearchParamRegistry.getActiveSearchParams(theResourceType);
|
ResourceSearchParams resourceActiveSearchParams = theSearchParamRegistry.getActiveSearchParams(theResourceType);
|
||||||
for (String paramName : myParams.keySet()) {
|
for (String paramName : myParams.keySet()) {
|
||||||
// is this parameter supported?
|
// is this parameter supported?
|
||||||
|
|
|
@ -43,6 +43,9 @@ import java.time.ZoneId;
|
||||||
@Scope("prototype")
|
@Scope("prototype")
|
||||||
public class ResourceChangeListenerCache implements IResourceChangeListenerCache {
|
public class ResourceChangeListenerCache implements IResourceChangeListenerCache {
|
||||||
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerCache.class);
|
private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerCache.class);
|
||||||
|
/**
|
||||||
|
* Max number of retries to do for cache refreshing
|
||||||
|
*/
|
||||||
private static final int MAX_RETRIES = 60;
|
private static final int MAX_RETRIES = 60;
|
||||||
|
|
||||||
private static Instant ourNowForUnitTests;
|
private static Instant ourNowForUnitTests;
|
||||||
|
@ -123,7 +126,7 @@ public class ResourceChangeListenerCache implements IResourceChangeListenerCache
|
||||||
return myNextRefreshTime.isBefore(now());
|
return myNextRefreshTime.isBefore(now());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Instant now() {
|
static Instant now() {
|
||||||
if (ourNowForUnitTests != null) {
|
if (ourNowForUnitTests != null) {
|
||||||
return ourNowForUnitTests;
|
return ourNowForUnitTests;
|
||||||
}
|
}
|
||||||
|
@ -153,7 +156,7 @@ public class ResourceChangeListenerCache implements IResourceChangeListenerCache
|
||||||
return myResourceChangeListenerCacheRefresher.refreshCacheAndNotifyListener(this);
|
return myResourceChangeListenerCacheRefresher.refreshCacheAndNotifyListener(this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MAX_RETRIES);
|
getMaxRetries());
|
||||||
return refreshCacheRetrier.runWithRetry();
|
return refreshCacheRetrier.runWithRetry();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,4 +226,8 @@ public class ResourceChangeListenerCache implements IResourceChangeListenerCache
|
||||||
.append("myInitialized", myInitialized)
|
.append("myInitialized", myInitialized)
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int getMaxRetries() {
|
||||||
|
return MAX_RETRIES;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,6 +149,7 @@ public class ResourceChangeListenerCacheRefresherImpl
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
SearchParameterMap searchParamMap = theCache.getSearchParameterMap();
|
SearchParameterMap searchParamMap = theCache.getSearchParameterMap();
|
||||||
|
|
||||||
ResourceVersionMap newResourceVersionMap =
|
ResourceVersionMap newResourceVersionMap =
|
||||||
myResourceVersionSvc.getVersionMap(theCache.getResourceName(), searchParamMap);
|
myResourceVersionSvc.getVersionMap(theCache.getResourceName(), searchParamMap);
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,7 @@ public class SearchParamRegistryImpl
|
||||||
|
|
||||||
private void requiresActiveSearchParams() {
|
private void requiresActiveSearchParams() {
|
||||||
if (myActiveSearchParams == null) {
|
if (myActiveSearchParams == null) {
|
||||||
|
// forced refreshes should not use a cache - we're forcibly refrsching it, after all
|
||||||
myResourceChangeListenerCache.forceRefresh();
|
myResourceChangeListenerCache.forceRefresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,7 +221,7 @@ public class SearchParamRegistryImpl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
myActiveSearchParams = searchParams;
|
setActiveSearchParams(searchParams);
|
||||||
|
|
||||||
myJpaSearchParamCache.populateActiveSearchParams(
|
myJpaSearchParamCache.populateActiveSearchParams(
|
||||||
myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams);
|
myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams);
|
||||||
|
@ -422,9 +423,15 @@ public class SearchParamRegistryImpl
|
||||||
initializeActiveSearchParams(searchParams);
|
initializeActiveSearchParams(searchParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return myActiveSearchParams != null;
|
||||||
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public void resetForUnitTest() {
|
public void resetForUnitTest() {
|
||||||
myBuiltInSearchParams = null;
|
myBuiltInSearchParams = null;
|
||||||
|
setActiveSearchParams(null);
|
||||||
handleInit(Collections.emptyList());
|
handleInit(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,4 +445,9 @@ public class SearchParamRegistryImpl
|
||||||
public int getMaxManagedParamCountForUnitTests() {
|
public int getMaxManagedParamCountForUnitTests() {
|
||||||
return MAX_MANAGED_PARAM_COUNT;
|
return MAX_MANAGED_PARAM_COUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public void setActiveSearchParams(RuntimeSearchParamCache theSearchParams) {
|
||||||
|
myActiveSearchParams = theSearchParams;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.cache;
|
||||||
import ca.uhn.fhir.i18n.Msg;
|
import ca.uhn.fhir.i18n.Msg;
|
||||||
import ca.uhn.fhir.interceptor.api.HookParams;
|
import ca.uhn.fhir.interceptor.api.HookParams;
|
||||||
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
|
||||||
|
import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl;
|
||||||
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
import ca.uhn.fhir.jpa.test.BaseJpaR4Test;
|
||||||
import ca.uhn.fhir.model.primitive.IdDt;
|
import ca.uhn.fhir.model.primitive.IdDt;
|
||||||
import ca.uhn.fhir.rest.param.DateRangeParam;
|
import ca.uhn.fhir.rest.param.DateRangeParam;
|
||||||
|
@ -17,6 +18,7 @@ import org.hl7.fhir.r4.model.Patient;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -24,13 +26,18 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
|
|
||||||
public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
|
public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
|
||||||
private static final long TEST_REFRESH_INTERVAL = DateUtils.MILLIS_PER_DAY;
|
private static final long TEST_REFRESH_INTERVAL = DateUtils.MILLIS_PER_DAY;
|
||||||
|
@ -39,6 +46,9 @@ public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
|
||||||
@Autowired
|
@Autowired
|
||||||
IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher;
|
IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SearchParamRegistryImpl mySearchParamRegistry;
|
||||||
|
|
||||||
private final static String RESOURCE_NAME = "Patient";
|
private final static String RESOURCE_NAME = "Patient";
|
||||||
private TestCallback myMaleTestCallback = new TestCallback("MALE");
|
private TestCallback myMaleTestCallback = new TestCallback("MALE");
|
||||||
private TestCallback myFemaleTestCallback = new TestCallback("FEMALE");
|
private TestCallback myFemaleTestCallback = new TestCallback("FEMALE");
|
||||||
|
@ -54,6 +64,63 @@ public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
|
||||||
myResourceChangeListenerRegistry.clearCachesForUnitTest();
|
myResourceChangeListenerRegistry.clearCachesForUnitTest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void forceRefresh_beforeSearchParametersInitialized_willNotFallIntoAnInfiniteLoop() {
|
||||||
|
// setup
|
||||||
|
int maxRetries = 3; // a small number for short tests
|
||||||
|
AtomicInteger counter = new AtomicInteger();
|
||||||
|
AtomicBoolean initCheck = new AtomicBoolean();
|
||||||
|
|
||||||
|
IResourceChangeListenerCache cache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener(
|
||||||
|
"StructureDefinition",
|
||||||
|
SearchParameterMap.newSynchronous(),
|
||||||
|
new IResourceChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void handleInit(Collection<IIdType> theResourceIds) {
|
||||||
|
initCheck.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleChange(IResourceChangeEvent theResourceChangeEvent) {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
assertTrue((cache instanceof ResourceChangeListenerCache));
|
||||||
|
|
||||||
|
// set the HSearchIndexing
|
||||||
|
boolean useAdvancedHSearch = myStorageSettings.isAdvancedHSearchIndexing();
|
||||||
|
myStorageSettings.setAdvancedHSearchIndexing(true);
|
||||||
|
|
||||||
|
// so they will be forced to be refreshed
|
||||||
|
mySearchParamRegistry.setActiveSearchParams(null);
|
||||||
|
try (MockedStatic<ResourceChangeListenerCache> cacheConstants = mockStatic(ResourceChangeListenerCache.class)) {
|
||||||
|
cacheConstants.when(ResourceChangeListenerCache::getMaxRetries).thenAnswer((args) -> {
|
||||||
|
if (counter.getAndIncrement() > maxRetries) {
|
||||||
|
// fail after a few tries to ensure we don't fall into an infinite loop
|
||||||
|
fail("This should not fall into an infinite loop");
|
||||||
|
}
|
||||||
|
return maxRetries;
|
||||||
|
});
|
||||||
|
cacheConstants.when(ResourceChangeListenerCache::now)
|
||||||
|
.thenCallRealMethod();
|
||||||
|
|
||||||
|
// test
|
||||||
|
ResourceChangeResult result = cache.forceRefresh();
|
||||||
|
|
||||||
|
// verify
|
||||||
|
assertEquals(0, result.created);
|
||||||
|
assertEquals(0, result.updated);
|
||||||
|
assertEquals(0, result.deleted);
|
||||||
|
assertTrue(initCheck.get());
|
||||||
|
} finally {
|
||||||
|
// reset for other tests
|
||||||
|
myStorageSettings.setAdvancedHSearchIndexing(useAdvancedHSearch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRegisterListener() throws InterruptedException {
|
public void testRegisterListener() throws InterruptedException {
|
||||||
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
|
||||||
|
|
|
@ -39,6 +39,16 @@ import java.util.TreeSet;
|
||||||
// TODO: JA remove default methods
|
// TODO: JA remove default methods
|
||||||
public interface ISearchParamRegistry {
|
public interface ISearchParamRegistry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if this registry is initialized and ready to handle
|
||||||
|
* searches and use its cache.
|
||||||
|
* Return false if cache has not been initialized.
|
||||||
|
*/
|
||||||
|
default boolean isInitialized() {
|
||||||
|
// default initialized to not break current implementers
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Returns {@literal null} if no match
|
* @return Returns {@literal null} if no match
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue