Avoid long pauses in CapabilityStatement generation (#3212)
* Avoid long pauses in CapabilityStatement generation * Add changelog * Test fix
This commit is contained in:
parent
2fbbf31431
commit
84a9609236
|
@ -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."
|
|
@ -7,6 +7,7 @@ import ca.uhn.fhir.context.support.TranslateConceptResults;
|
|||
import ca.uhn.fhir.context.support.ValidationSupportContext;
|
||||
import com.github.benmanes.caffeine.cache.Cache;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||
|
@ -15,8 +16,14 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.function.Function;
|
||||
|
||||
|
@ -28,10 +35,13 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport {
|
||||
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class);
|
||||
|
||||
private final Cache<String, Object> myCache;
|
||||
private final Cache<String, Object> myValidateCodeCache;
|
||||
private final Cache<TranslateCodeRequest, Object> myTranslateCodeCache;
|
||||
private final Cache<String, Object> myLookupCodeCache;
|
||||
private final ThreadPoolExecutor myBackgroundExecutor;
|
||||
private final Map<Object, Object> myNonExpiringCache;
|
||||
|
||||
/**
|
||||
* Constuctor with default timeouts
|
||||
|
@ -70,24 +80,41 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
|
|||
.expireAfterWrite(theCacheTimeouts.getMiscMillis(), TimeUnit.MILLISECONDS)
|
||||
.maximumSize(5000)
|
||||
.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
|
||||
public List<IBaseResource> fetchAllConformanceResources() {
|
||||
String key = "fetchAllConformanceResources";
|
||||
return loadFromCache(myCache, key, t -> super.fetchAllConformanceResources());
|
||||
return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllConformanceResources());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
|
||||
String key = "fetchAllStructureDefinitions";
|
||||
return loadFromCache(myCache, key, t -> super.fetchAllStructureDefinitions());
|
||||
return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllStructureDefinitions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() {
|
||||
String key = "fetchAllNonBaseStructureDefinitions";
|
||||
return loadFromCache(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions());
|
||||
return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -159,14 +186,36 @@ public class CachingValidationSupport extends BaseValidationSupportWrapper imple
|
|||
assert result != 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
|
||||
public void invalidateCaches() {
|
||||
myLookupCodeCache.invalidateAll();
|
||||
myCache.invalidateAll();
|
||||
myValidateCodeCache.invalidateAll();
|
||||
myNonExpiringCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue