From 84a9609236ebdf41707a844f65651f62fe39997e Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 3 Dec 2021 05:28:04 -0500 Subject: [PATCH] Avoid long pauses in CapabilityStatement generation (#3212) * Avoid long pauses in CapabilityStatement generation * Add changelog * Test fix --- ...12-avoid-long-pauses-in-cs-generation.yaml | 4 ++ .../support/CachingValidationSupport.java | 57 +++++++++++++-- .../support/CachingValidationSupportTest.java | 72 +++++++++++++++++++ 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3212-avoid-long-pauses-in-cs-generation.yaml create mode 100644 hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3212-avoid-long-pauses-in-cs-generation.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3212-avoid-long-pauses-in-cs-generation.yaml new file mode 100644 index 00000000000..56f841568c5 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_7_0/3212-avoid-long-pauses-in-cs-generation.yaml @@ -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." diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java index 635a717da45..a3089c9ba6e 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupport.java @@ -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 myCache; private final Cache myValidateCodeCache; private final Cache myTranslateCodeCache; private final Cache myLookupCodeCache; + private final ThreadPoolExecutor myBackgroundExecutor; + private final Map 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 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 fetchAllConformanceResources() { String key = "fetchAllConformanceResources"; - return loadFromCache(myCache, key, t -> super.fetchAllConformanceResources()); + return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllConformanceResources()); } @Override public List fetchAllStructureDefinitions() { String key = "fetchAllStructureDefinitions"; - return loadFromCache(myCache, key, t -> super.fetchAllStructureDefinitions()); + return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllStructureDefinitions()); } @Override public List 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 T loadFromCacheWithAsyncRefresh(Cache theCache, S theKey, Function 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(); } /** diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java new file mode 100644 index 00000000000..87de9f373f7 --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/common/hapi/validation/support/CachingValidationSupportTest.java @@ -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 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 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()); + } + + +}