merge fix for issue with infinite cache refresh loop

This commit is contained in:
TipzCM 2024-06-27 09:27:33 -04:00 committed by longma1
parent 361f6d2983
commit bc47cb70f1
8 changed files with 118 additions and 3 deletions

View File

@ -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
"

View File

@ -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);
} }

View File

@ -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?

View File

@ -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;
}
} }

View File

@ -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);

View File

@ -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;
}
} }

View File

@ -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());

View File

@ -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
*/ */