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