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