Avoid long pauses in CapabilityStatement generation (#3212)

* Avoid long pauses in CapabilityStatement generation

* Add changelog

* Test fix
This commit is contained in:
James Agnew 2021-12-03 05:28:04 -05:00 committed by GitHub
parent 2fbbf31431
commit 84a9609236
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 129 additions and 4 deletions

View File

@ -0,0 +1,4 @@
---
type: fix
title: "In servers with a large number of StructureDefinition resources loaded, occasionally a call for the
CapabilityStatement could take a very long time to return. This has been corrected."

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.context.support.TranslateConceptResults;
import ca.uhn.fhir.context.support.ValidationSupportContext; import ca.uhn.fhir.context.support.ValidationSupportContext;
import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.instance.model.api.IPrimitiveType;
@ -15,8 +16,14 @@ import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Function; import java.util.function.Function;
@ -28,10 +35,13 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport { public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport {
private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class); private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class);
private final Cache<String, Object> myCache; private final Cache<String, Object> myCache;
private final Cache<String, Object> myValidateCodeCache; private final Cache<String, Object> myValidateCodeCache;
private final Cache<TranslateCodeRequest, Object> myTranslateCodeCache; private final Cache<TranslateCodeRequest, Object> myTranslateCodeCache;
private final Cache<String, Object> myLookupCodeCache; private final Cache<String, Object> myLookupCodeCache;
private final ThreadPoolExecutor myBackgroundExecutor;
private final Map<Object, Object> myNonExpiringCache;
/** /**
* Constuctor with default timeouts * Constuctor with default timeouts
@ -70,24 +80,41 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
.expireAfterWrite(theCacheTimeouts.getMiscMillis(), TimeUnit.MILLISECONDS) .expireAfterWrite(theCacheTimeouts.getMiscMillis(), TimeUnit.MILLISECONDS)
.maximumSize(5000) .maximumSize(5000)
.build(); .build();
myNonExpiringCache = Collections.synchronizedMap(new HashMap<>());
LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000);
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern("CachingValidationSupport-%d")
.daemon(false)
.priority(Thread.NORM_PRIORITY)
.build();
myBackgroundExecutor = new ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
executorQueue,
threadFactory,
new ThreadPoolExecutor.DiscardPolicy());
} }
@Override @Override
public List<IBaseResource> fetchAllConformanceResources() { public List<IBaseResource> fetchAllConformanceResources() {
String key = "fetchAllConformanceResources"; String key = "fetchAllConformanceResources";
return loadFromCache(myCache, key, t -> super.fetchAllConformanceResources()); return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllConformanceResources());
} }
@Override @Override
public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() { public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
String key = "fetchAllStructureDefinitions"; String key = "fetchAllStructureDefinitions";
return loadFromCache(myCache, key, t -> super.fetchAllStructureDefinitions()); return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllStructureDefinitions());
} }
@Override @Override
public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() { public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() {
String key = "fetchAllNonBaseStructureDefinitions"; String key = "fetchAllNonBaseStructureDefinitions";
return loadFromCache(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions()); return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions());
} }
@Override @Override
@ -159,14 +186,36 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
assert result != null; assert result != null;
return result.orElse(null); return result.orElse(null);
} }
private <S, T> T loadFromCacheWithAsyncRefresh(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
T retVal = (T) theCache.getIfPresent(theKey);
if (retVal == null) {
retVal = (T) myNonExpiringCache.get(theKey);
if (retVal != null) {
Runnable loaderTask = ()->{
T loadedItem = loadFromCache(theCache, theKey, theLoader);
myNonExpiringCache.put(theKey, loadedItem);
};
myBackgroundExecutor.execute(loaderTask);
return retVal;
}
}
retVal = loadFromCache(theCache, theKey, theLoader);
myNonExpiringCache.put(theKey, retVal);
return retVal;
}
@Override @Override
public void invalidateCaches() { public void invalidateCaches() {
myLookupCodeCache.invalidateAll(); myLookupCodeCache.invalidateAll();
myCache.invalidateAll(); myCache.invalidateAll();
myValidateCodeCache.invalidateAll(); myValidateCodeCache.invalidateAll();
myNonExpiringCache.clear();
} }
/** /**

View File

@ -0,0 +1,72 @@
package org.hl7.fhir.common.hapi.validation.support;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import com.google.common.collect.Lists;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Collections;
import java.util.List;
import static ca.uhn.fhir.util.TestUtil.sleepAtLeast;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class CachingValidationSupportTest {
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@Mock
private IValidationSupport myValidationSupport;
@Test
public void testAsyncBackgroundLoading() {
StructureDefinition sd0 = (StructureDefinition) new StructureDefinition().setId("SD0");
StructureDefinition sd1 = (StructureDefinition) new StructureDefinition().setId("SD1");
StructureDefinition sd2 = (StructureDefinition) new StructureDefinition().setId("SD2");
List<StructureDefinition> responses = Collections.synchronizedList(Lists.newArrayList(
sd0, sd1, sd2
));
when(myValidationSupport.getFhirContext()).thenReturn(ourCtx);
when(myValidationSupport.fetchAllNonBaseStructureDefinitions()).thenAnswer(t -> {
Thread.sleep(2000);
return Collections.singletonList(responses.remove(0));
});
CachingValidationSupport.CacheTimeouts cacheTimeouts = CachingValidationSupport.CacheTimeouts
.defaultValues()
.setMiscMillis(1000);
CachingValidationSupport support = new CachingValidationSupport(myValidationSupport, cacheTimeouts);
assertEquals(3, responses.size());
List<IBaseResource> fetched = support.fetchAllNonBaseStructureDefinitions();
assert fetched != null;
assertSame(sd0, fetched.get(0));
assertEquals(2, responses.size());
sleepAtLeast(1200);
fetched = support.fetchAllNonBaseStructureDefinitions();
assert fetched != null;
assertSame(sd0, fetched.get(0));
assertEquals(2, responses.size());
await().until(() -> responses.size(), equalTo(1));
assertEquals(1, responses.size());
fetched = support.fetchAllNonBaseStructureDefinitions();
assert fetched != null;
assertSame(sd1, fetched.get(0));
assertEquals(1, responses.size());
}
}