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}