001package org.hl7.fhir.common.hapi.validation.support;
002
003import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
004import ca.uhn.fhir.context.support.ConceptValidationOptions;
005import ca.uhn.fhir.context.support.IValidationSupport;
006import ca.uhn.fhir.context.support.TranslateConceptResults;
007import ca.uhn.fhir.context.support.ValidationSupportContext;
008import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
009import com.github.benmanes.caffeine.cache.Cache;
010import com.github.benmanes.caffeine.cache.Caffeine;
011import org.apache.commons.lang3.concurrent.BasicThreadFactory;
012import org.apache.commons.lang3.time.DateUtils;
013import org.hl7.fhir.instance.model.api.IBaseResource;
014import org.hl7.fhir.instance.model.api.IPrimitiveType;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018import javax.annotation.Nonnull;
019import javax.annotation.Nullable;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Optional;
025import java.util.concurrent.LinkedBlockingQueue;
026import java.util.concurrent.ThreadPoolExecutor;
027import java.util.concurrent.TimeUnit;
028import java.util.function.Function;
029
030import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
031import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
032import static org.apache.commons.lang3.StringUtils.defaultString;
033import static org.apache.commons.lang3.StringUtils.isNotBlank;
034
035@SuppressWarnings("unchecked")
036public class CachingValidationSupport extends BaseValidationSupportWrapper implements IValidationSupport {
037
038        private static final Logger ourLog = LoggerFactory.getLogger(CachingValidationSupport.class);
039        public static final ValueSetExpansionOptions EMPTY_EXPANSION_OPTIONS = new ValueSetExpansionOptions();
040
041        private final Cache<String, Object> myCache;
042        private final Cache<String, Object> myValidateCodeCache;
043        private final Cache<TranslateCodeRequest, Object> myTranslateCodeCache;
044        private final Cache<String, Object> myLookupCodeCache;
045        private final ThreadPoolExecutor myBackgroundExecutor;
046        private final Map<Object, Object> myNonExpiringCache;
047        private final Cache<String, Object> myExpandValueSetCache;
048
049        /**
050         * Constructor with default timeouts
051         *
052         * @param theWrap The validation support module to wrap
053         */
054        public CachingValidationSupport(IValidationSupport theWrap) {
055                this(theWrap, CacheTimeouts.defaultValues());
056        }
057
058        /**
059         * Constructor with configurable timeouts
060         *
061         * @param theWrap          The validation support module to wrap
062         * @param theCacheTimeouts The timeouts to use
063         */
064        public CachingValidationSupport(IValidationSupport theWrap, CacheTimeouts theCacheTimeouts) {
065                super(theWrap.getFhirContext(), theWrap);
066                myExpandValueSetCache = Caffeine
067                        .newBuilder()
068                        .expireAfterWrite(theCacheTimeouts.getExpandValueSetMillis(), TimeUnit.MILLISECONDS)
069                        .maximumSize(100)
070                        .build();
071                myValidateCodeCache = Caffeine
072                        .newBuilder()
073                        .expireAfterWrite(theCacheTimeouts.getValidateCodeMillis(), TimeUnit.MILLISECONDS)
074                        .maximumSize(5000)
075                        .build();
076                myLookupCodeCache = Caffeine
077                        .newBuilder()
078                        .expireAfterWrite(theCacheTimeouts.getLookupCodeMillis(), TimeUnit.MILLISECONDS)
079                        .maximumSize(5000)
080                        .build();
081                myTranslateCodeCache = Caffeine
082                        .newBuilder()
083                        .expireAfterWrite(theCacheTimeouts.getTranslateCodeMillis(), TimeUnit.MILLISECONDS)
084                        .maximumSize(5000)
085                        .build();
086                myCache = Caffeine
087                        .newBuilder()
088                        .expireAfterWrite(theCacheTimeouts.getMiscMillis(), TimeUnit.MILLISECONDS)
089                        .maximumSize(5000)
090                        .build();
091                myNonExpiringCache = Collections.synchronizedMap(new HashMap<>());
092
093                LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<>(1000);
094                BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
095                        .namingPattern("CachingValidationSupport-%d")
096                        .daemon(false)
097                        .priority(Thread.NORM_PRIORITY)
098                        .build();
099                myBackgroundExecutor = new ThreadPoolExecutor(
100                        1,
101                        1,
102                        0L,
103                        TimeUnit.MILLISECONDS,
104                        executorQueue,
105                        threadFactory,
106                        new ThreadPoolExecutor.DiscardPolicy());
107
108        }
109
110        @Override
111        public List<IBaseResource> fetchAllConformanceResources() {
112                String key = "fetchAllConformanceResources";
113                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllConformanceResources());
114        }
115
116        @Override
117        public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
118                String key = "fetchAllStructureDefinitions";
119                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllStructureDefinitions());
120        }
121
122        @Override
123        public <T extends IBaseResource> List<T> fetchAllNonBaseStructureDefinitions() {
124                String key = "fetchAllNonBaseStructureDefinitions";
125                return loadFromCacheWithAsyncRefresh(myCache, key, t -> super.fetchAllNonBaseStructureDefinitions());
126        }
127
128        @Override
129        public IBaseResource fetchCodeSystem(String theSystem) {
130                return loadFromCache(myCache, "fetchCodeSystem " + theSystem, t -> super.fetchCodeSystem(theSystem));
131        }
132
133        @Override
134        public IBaseResource fetchValueSet(String theUri) {
135                return loadFromCache(myCache, "fetchValueSet " + theUri, t -> super.fetchValueSet(theUri));
136        }
137
138        @Override
139        public IBaseResource fetchStructureDefinition(String theUrl) {
140                return loadFromCache(myCache, "fetchStructureDefinition " + theUrl, t -> super.fetchStructureDefinition(theUrl));
141        }
142
143        @Override
144        public byte[] fetchBinary(String theBinaryKey) {
145                return loadFromCache(myCache, "fetchBinary " + theBinaryKey, t -> super.fetchBinary(theBinaryKey));
146        }
147
148        @Override
149        public <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
150                return loadFromCache(myCache, "fetchResource " + theClass + " " + theUri,
151                        t -> super.fetchResource(theClass, theUri));
152        }
153
154        @Override
155        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
156                String key = "isCodeSystemSupported " + theSystem;
157                Boolean retVal = loadFromCacheReentrantSafe(myCache, key, t -> super.isCodeSystemSupported(theValidationSupportContext, theSystem));
158                assert retVal != null;
159                return retVal;
160        }
161
162        @Override
163        public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
164                if (!theValueSetToExpand.getIdElement().hasIdPart()) {
165                        return super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand);
166                }
167
168                ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS);
169                String key = "expandValueSet " +
170                        theValueSetToExpand.getIdElement().getValue() + " " +
171                        expansionOptions.isIncludeHierarchy() + " " +
172                        expansionOptions.getFilter() + " " +
173                        expansionOptions.getOffset() + " " +
174                        expansionOptions.getCount();
175                return loadFromCache(myExpandValueSetCache, key, t -> super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand));
176        }
177
178        @Override
179        public CodeValidationResult validateCode(@Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
180                String key = "validateCode " + theCodeSystem + " " + theCode + " " + defaultIfBlank(theValueSetUrl, "NO_VS");
181                return loadFromCache(myValidateCodeCache, key, t -> super.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl));
182        }
183
184        @Override
185        public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) {
186                String key = "lookupCode " + theSystem + " " + theCode + " " + defaultIfBlank(theDisplayLanguage, "NO_LANG");
187                return loadFromCache(myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theSystem, theCode, theDisplayLanguage));
188        }
189
190        @Override
191        public IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
192
193                BaseRuntimeChildDefinition urlChild = myCtx.getResourceDefinition(theValueSet).getChildByName("url");
194                Optional<String> valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream().map(t -> ((IPrimitiveType<?>) t).getValueAsString()).filter(t -> isNotBlank(t)).findFirst();
195                if (valueSetUrl.isPresent()) {
196                        String key = "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem, "(null)") + " " + defaultString(theCode, "(null)") + " " + defaultString(theDisplay, "(null)") + " " + valueSetUrl.get();
197                        return loadFromCache(myValidateCodeCache, key, t -> super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet));
198                }
199
200                return super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet);
201        }
202
203        @Override
204        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
205                return loadFromCache(myTranslateCodeCache, theRequest, k -> super.translateConcept(theRequest));
206        }
207
208        @SuppressWarnings("OptionalAssignedToNull")
209        @Nullable
210        private <S, T> T loadFromCache(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
211                ourLog.trace("Fetching from cache: {}", theKey);
212
213                Function<S, Optional<T>> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey));
214                Optional<T> result = (Optional<T>) theCache.get(theKey, loaderWrapper);
215                assert result != null;
216
217                return result.orElse(null);
218        }
219
220        /**
221         * The Caffeine cache uses ConcurrentHashMap which is not reentrant, so if we get unlucky and the hashtable
222         * needs to grow at the same time as we are in a reentrant cache lookup, the thread will deadlock.  Use this
223         * method in place of loadFromCache in situations where a cache lookup calls another cache lookup within its lambda
224         */
225        @Nullable
226        private <S, T> T loadFromCacheReentrantSafe(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
227                ourLog.trace("Reentrant fetch from cache: {}", theKey);
228
229                Optional<T> result = (Optional<T>) theCache.getIfPresent(theKey);
230                if (result != null && result.isPresent()) {
231                        return result.get();
232                }
233                T value = theLoader.apply(theKey);
234                assert value != null;
235
236                theCache.put(theKey, Optional.of(value));
237
238                return value;
239        }
240
241        private <S, T> T loadFromCacheWithAsyncRefresh(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
242                T retVal = (T) theCache.getIfPresent(theKey);
243                if (retVal == null) {
244                        retVal = (T) myNonExpiringCache.get(theKey);
245                        if (retVal != null) {
246
247                                Runnable loaderTask = () -> {
248                                        T loadedItem = loadFromCache(theCache, theKey, theLoader);
249                                        myNonExpiringCache.put(theKey, loadedItem);
250                                };
251                                myBackgroundExecutor.execute(loaderTask);
252
253                                return retVal;
254                        }
255                }
256
257                retVal = loadFromCache(theCache, theKey, theLoader);
258                myNonExpiringCache.put(theKey, retVal);
259                return retVal;
260        }
261
262
263        @Override
264        public void invalidateCaches() {
265                myExpandValueSetCache.invalidateAll();
266                myLookupCodeCache.invalidateAll();
267                myCache.invalidateAll();
268                myValidateCodeCache.invalidateAll();
269                myNonExpiringCache.clear();
270        }
271
272        /**
273         * @since 5.4.0
274         */
275        public static class CacheTimeouts {
276
277                private long myTranslateCodeMillis;
278                private long myLookupCodeMillis;
279                private long myValidateCodeMillis;
280                private long myMiscMillis;
281                private long myExpandValueSetMillis;
282
283                public long getExpandValueSetMillis() {
284                        return myExpandValueSetMillis;
285                }
286
287                public CacheTimeouts setExpandValueSetMillis(long theExpandValueSetMillis) {
288                        myExpandValueSetMillis = theExpandValueSetMillis;
289                        return this;
290                }
291
292                public long getTranslateCodeMillis() {
293                        return myTranslateCodeMillis;
294                }
295
296                public CacheTimeouts setTranslateCodeMillis(long theTranslateCodeMillis) {
297                        myTranslateCodeMillis = theTranslateCodeMillis;
298                        return this;
299                }
300
301                public long getLookupCodeMillis() {
302                        return myLookupCodeMillis;
303                }
304
305                public CacheTimeouts setLookupCodeMillis(long theLookupCodeMillis) {
306                        myLookupCodeMillis = theLookupCodeMillis;
307                        return this;
308                }
309
310                public long getValidateCodeMillis() {
311                        return myValidateCodeMillis;
312                }
313
314                public CacheTimeouts setValidateCodeMillis(long theValidateCodeMillis) {
315                        myValidateCodeMillis = theValidateCodeMillis;
316                        return this;
317                }
318
319                public long getMiscMillis() {
320                        return myMiscMillis;
321                }
322
323                public CacheTimeouts setMiscMillis(long theMiscMillis) {
324                        myMiscMillis = theMiscMillis;
325                        return this;
326                }
327
328                public static CacheTimeouts defaultValues() {
329                        return new CacheTimeouts()
330                                .setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
331                                .setExpandValueSetMillis(1 * DateUtils.MILLIS_PER_MINUTE)
332                                .setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
333                                .setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
334                                .setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE);
335                }
336        }
337}