Skip to content

Commit

Permalink
fixing issue with infinite cache refresh loop (hapifhir#6045)
Browse files Browse the repository at this point in the history
* fixing step 1

* spotless

* cleanup

* cleanup

* cleanup

* review fixes

* review fix

---------

Co-authored-by: leif stawnyczy <[email protected]>
  • Loading branch information
TipzCM and leif stawnyczy authored Jun 27, 2024
1 parent 7224245 commit 90251b3
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
type: fix
issue: 6044
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
"
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@ public boolean canUseHibernateSearch(String theResourceType, SearchParameterMap
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()
&& myAdvancedIndexQueryBuilder.canUseHibernateSearch(theResourceType, myParams, mySearchParamRegistry);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public boolean supportsSearchParameter(String theParamName, ResourceSearchParams
public boolean canUseHibernateSearch(
String theResourceType, SearchParameterMap myParams, ISearchParamRegistry theSearchParamRegistry) {
boolean canUseHibernate = true;

ResourceSearchParams resourceActiveSearchParams = theSearchParamRegistry.getActiveSearchParams(theResourceType);
for (String paramName : myParams.keySet()) {
// is this parameter supported?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
@Scope("prototype")
public class ResourceChangeListenerCache implements IResourceChangeListenerCache {
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 Instant ourNowForUnitTests;
Expand Down Expand Up @@ -123,7 +126,7 @@ protected boolean isTimeToRefresh() {
return myNextRefreshTime.isBefore(now());
}

private static Instant now() {
static Instant now() {
if (ourNowForUnitTests != null) {
return ourNowForUnitTests;
}
Expand Down Expand Up @@ -153,7 +156,7 @@ private ResourceChangeResult refreshCacheAndNotifyListenersWithRetry() {
return myResourceChangeListenerCacheRefresher.refreshCacheAndNotifyListener(this);
}
},
MAX_RETRIES);
getMaxRetries());
return refreshCacheRetrier.runWithRetry();
}

Expand Down Expand Up @@ -223,4 +226,8 @@ public String toString() {
.append("myInitialized", myInitialized)
.toString();
}

static int getMaxRetries() {
return MAX_RETRIES;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ public ResourceChangeResult refreshCacheAndNotifyListener(IResourceChangeListene
return retVal;
}
SearchParameterMap searchParamMap = theCache.getSearchParameterMap();

ResourceVersionMap newResourceVersionMap =
myResourceVersionSvc.getVersionMap(theCache.getResourceName(), searchParamMap);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public ResourceSearchParams getActiveSearchParams(String theResourceName) {

private void requiresActiveSearchParams() {
if (myActiveSearchParams == null) {
// forced refreshes should not use a cache - we're forcibly refrsching it, after all
myResourceChangeListenerCache.forceRefresh();
}
}
Expand Down Expand Up @@ -230,7 +231,7 @@ private void initializeActiveSearchParams(Collection<IBaseResource> theJpaSearch
}
}

myActiveSearchParams = searchParams;
setActiveSearchParams(searchParams);

myJpaSearchParamCache.populateActiveSearchParams(
myInterceptorBroadcaster, myPhoneticEncoder, myActiveSearchParams);
Expand Down Expand Up @@ -432,9 +433,15 @@ public void handleInit(Collection<IIdType> theResourceIds) {
initializeActiveSearchParams(searchParams);
}

@Override
public boolean isInitialized() {
return myActiveSearchParams != null;
}

@VisibleForTesting
public void resetForUnitTest() {
myBuiltInSearchParams = null;
setActiveSearchParams(null);
handleInit(Collections.emptyList());
}

Expand All @@ -448,4 +455,9 @@ public void setSearchParameterCanonicalizerForUnitTest(
public int getMaxManagedParamCountForUnitTests() {
return MAX_MANAGED_PARAM_COUNT;
}

@VisibleForTesting
public void setActiveSearchParams(RuntimeSearchParamCache theSearchParams) {
myActiveSearchParams = theSearchParams;
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package ca.uhn.fhir.jpa.cache;

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 ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
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.model.primitive.IdDt;
import ca.uhn.fhir.rest.param.DateRangeParam;
Expand All @@ -20,17 +18,24 @@
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
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 java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.Assertions.assertThat;
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.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 {
Expand All @@ -40,6 +45,9 @@ public class ResourceChangeListenerRegistryImplIT extends BaseJpaR4Test {
@Autowired
IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher;

@Autowired
private SearchParamRegistryImpl mySearchParamRegistry;

private final static String RESOURCE_NAME = "Patient";
private TestCallback myMaleTestCallback = new TestCallback("MALE");
private TestCallback myFemaleTestCallback = new TestCallback("FEMALE");
Expand All @@ -55,6 +63,63 @@ public void after() {
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
public void testRegisterListener() throws InterruptedException {
assertEquals(0, myResourceChangeListenerRegistry.getResourceVersionCacheSizeForUnitTest());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package ca.uhn.fhir.jpa.dao.r4;

import static org.junit.jupiter.api.Assertions.assertNull;
import ca.uhn.fhir.batch2.api.IJobDataSink;
import ca.uhn.fhir.batch2.api.RunOutcome;
import ca.uhn.fhir.batch2.api.VoidModel;
Expand Down Expand Up @@ -106,14 +105,13 @@
import static ca.uhn.fhir.jpa.subscription.FhirR4Util.createSubscription;
import static org.apache.commons.lang3.StringUtils.countMatches;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.awaitility.Awaitility.await;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -155,7 +153,6 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test
protected SubscriptionTestUtil mySubscriptionTestUtil;
private ReindexTestHelper myReindexTestHelper;


@AfterEach
public void afterResetDao() {
mySubscriptionSettings.clearSupportedSubscriptionTypesForUnitTest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
// TODO: JA remove default methods
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
*/
Expand Down

0 comments on commit 90251b3

Please sign in to comment.