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}