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}