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         * Constuctor 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 <T extends IBaseResource> T fetchResource(@Nullable Class<T> theClass, String theUri) {
145                return loadFromCache(myCache, "fetchResource " + theClass + " " + theUri,
146                        t -> super.fetchResource(theClass, theUri));
147        }
148
149        @Override
150        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
151                String key = "isCodeSystemSupported " + theSystem;
152                Boolean retVal = loadFromCacheReentrantSafe(myCache, key, t -> super.isCodeSystemSupported(theValidationSupportContext, theSystem));
153                assert retVal != null;
154                return retVal;
155        }
156
157        @Override
158        public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
159                if (!theValueSetToExpand.getIdElement().hasIdPart()) {
160                        return super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand);
161                }
162
163                ValueSetExpansionOptions expansionOptions = defaultIfNull(theExpansionOptions, EMPTY_EXPANSION_OPTIONS);
164                String key = "expandValueSet " +
165                        theValueSetToExpand.getIdElement().getValue() + " " +
166                        expansionOptions.isIncludeHierarchy() + " " +
167                        expansionOptions.getFilter() + " " +
168                        expansionOptions.getOffset() + " " +
169                        expansionOptions.getCount();
170                return loadFromCache(myExpandValueSetCache, key, t -> super.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand));
171        }
172
173        @Override
174        public CodeValidationResult validateCode(@Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
175                String key = "validateCode " + theCodeSystem + " " + theCode + " " + defaultIfBlank(theValueSetUrl, "NO_VS");
176                return loadFromCache(myValidateCodeCache, key, t -> super.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl));
177        }
178
179        @Override
180        public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) {
181                String key = "lookupCode " + theSystem + " " + theCode + " " + defaultIfBlank(theDisplayLanguage, "NO_LANG");
182                return loadFromCache(myLookupCodeCache, key, t -> super.lookupCode(theValidationSupportContext, theSystem, theCode, theDisplayLanguage));
183        }
184
185        @Override
186        public IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
187
188                BaseRuntimeChildDefinition urlChild = myCtx.getResourceDefinition(theValueSet).getChildByName("url");
189                Optional<String> valueSetUrl = urlChild.getAccessor().getValues(theValueSet).stream().map(t -> ((IPrimitiveType<?>) t).getValueAsString()).filter(t -> isNotBlank(t)).findFirst();
190                if (valueSetUrl.isPresent()) {
191                        String key = "validateCodeInValueSet " + theValidationOptions.toString() + " " + defaultString(theCodeSystem, "(null)") + " " + defaultString(theCode, "(null)") + " " + defaultString(theDisplay, "(null)") + " " + valueSetUrl.get();
192                        return loadFromCache(myValidateCodeCache, key, t -> super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet));
193                }
194
195                return super.validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, theValueSet);
196        }
197
198        @Override
199        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
200                return loadFromCache(myTranslateCodeCache, theRequest, k -> super.translateConcept(theRequest));
201        }
202
203        @SuppressWarnings("OptionalAssignedToNull")
204        @Nullable
205        private <S, T> T loadFromCache(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
206                ourLog.trace("Fetching from cache: {}", theKey);
207
208                Function<S, Optional<T>> loaderWrapper = key -> Optional.ofNullable(theLoader.apply(theKey));
209                Optional<T> result = (Optional<T>) theCache.get(theKey, loaderWrapper);
210                assert result != null;
211
212                return result.orElse(null);
213        }
214
215        /**
216         * The Caffeine cache uses ConcurrentHashMap which is not reentrant, so if we get unlucky and the hashtable
217         * needs to grow at the same time as we are in a reentrant cache lookup, the thread will deadlock.  Use this
218         * method in place of loadFromCache in situations where a cache lookup calls another cache lookup within its lambda
219         */
220        @Nullable
221        private <S, T> T loadFromCacheReentrantSafe(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
222                ourLog.trace("Reentrant fetch from cache: {}", theKey);
223
224                Optional<T> result = (Optional<T>) theCache.getIfPresent(theKey);
225                if (result != null && result.isPresent()) {
226                        return result.get();
227                }
228                T value = theLoader.apply(theKey);
229                assert value != null;
230
231                theCache.put(theKey, Optional.of(value));
232
233                return value;
234        }
235
236        private <S, T> T loadFromCacheWithAsyncRefresh(Cache<S, Object> theCache, S theKey, Function<S, T> theLoader) {
237                T retVal = (T) theCache.getIfPresent(theKey);
238                if (retVal == null) {
239                        retVal = (T) myNonExpiringCache.get(theKey);
240                        if (retVal != null) {
241
242                                Runnable loaderTask = () -> {
243                                        T loadedItem = loadFromCache(theCache, theKey, theLoader);
244                                        myNonExpiringCache.put(theKey, loadedItem);
245                                };
246                                myBackgroundExecutor.execute(loaderTask);
247
248                                return retVal;
249                        }
250                }
251
252                retVal = loadFromCache(theCache, theKey, theLoader);
253                myNonExpiringCache.put(theKey, retVal);
254                return retVal;
255        }
256
257
258        @Override
259        public void invalidateCaches() {
260                myExpandValueSetCache.invalidateAll();
261                myLookupCodeCache.invalidateAll();
262                myCache.invalidateAll();
263                myValidateCodeCache.invalidateAll();
264                myNonExpiringCache.clear();
265        }
266
267        /**
268         * @since 5.4.0
269         */
270        public static class CacheTimeouts {
271
272                private long myTranslateCodeMillis;
273                private long myLookupCodeMillis;
274                private long myValidateCodeMillis;
275                private long myMiscMillis;
276                private long myExpandValueSetMillis;
277
278                public long getExpandValueSetMillis() {
279                        return myExpandValueSetMillis;
280                }
281
282                public CacheTimeouts setExpandValueSetMillis(long theExpandValueSetMillis) {
283                        myExpandValueSetMillis = theExpandValueSetMillis;
284                        return this;
285                }
286
287                public long getTranslateCodeMillis() {
288                        return myTranslateCodeMillis;
289                }
290
291                public CacheTimeouts setTranslateCodeMillis(long theTranslateCodeMillis) {
292                        myTranslateCodeMillis = theTranslateCodeMillis;
293                        return this;
294                }
295
296                public long getLookupCodeMillis() {
297                        return myLookupCodeMillis;
298                }
299
300                public CacheTimeouts setLookupCodeMillis(long theLookupCodeMillis) {
301                        myLookupCodeMillis = theLookupCodeMillis;
302                        return this;
303                }
304
305                public long getValidateCodeMillis() {
306                        return myValidateCodeMillis;
307                }
308
309                public CacheTimeouts setValidateCodeMillis(long theValidateCodeMillis) {
310                        myValidateCodeMillis = theValidateCodeMillis;
311                        return this;
312                }
313
314                public long getMiscMillis() {
315                        return myMiscMillis;
316                }
317
318                public CacheTimeouts setMiscMillis(long theMiscMillis) {
319                        myMiscMillis = theMiscMillis;
320                        return this;
321                }
322
323                public static CacheTimeouts defaultValues() {
324                        return new CacheTimeouts()
325                                .setLookupCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
326                                .setExpandValueSetMillis(1 * DateUtils.MILLIS_PER_MINUTE)
327                                .setTranslateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
328                                .setValidateCodeMillis(10 * DateUtils.MILLIS_PER_MINUTE)
329                                .setMiscMillis(10 * DateUtils.MILLIS_PER_MINUTE);
330                }
331        }
332}