001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.util; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.interceptor.model.RequestPartitionId; 024import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 025import ca.uhn.fhir.jpa.api.model.TranslationQuery; 026import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 027import ca.uhn.fhir.sl.cache.Cache; 028import ca.uhn.fhir.sl.cache.CacheFactory; 029import jakarta.annotation.Nonnull; 030import jakarta.annotation.Nullable; 031import org.apache.commons.lang3.builder.EqualsBuilder; 032import org.apache.commons.lang3.builder.HashCodeBuilder; 033import org.hl7.fhir.instance.model.api.IIdType; 034import org.springframework.transaction.support.TransactionSynchronization; 035import org.springframework.transaction.support.TransactionSynchronizationManager; 036 037import java.util.Collection; 038import java.util.EnumMap; 039import java.util.Map; 040import java.util.Objects; 041import java.util.function.Function; 042 043import static java.util.concurrent.TimeUnit.MINUTES; 044import static java.util.concurrent.TimeUnit.SECONDS; 045import static org.apache.commons.lang3.StringUtils.isNotBlank; 046 047/** 048 * This class acts as a central spot for all of the many Caffeine caches we use in HAPI FHIR. 049 * <p> 050 * The API is super simplistic, and caches are all 1-minute, max 10000 entries for starters. We could definitely add nuance to this, 051 * which will be much easier now that this is being centralized. Some logging/monitoring would be good too. 052 */ 053// TODO: JA2 extract an interface for this class and use it everywhere 054public class MemoryCacheService { 055 056 private final JpaStorageSettings myStorageSettings; 057 private final EnumMap<CacheEnum, Cache<?, ?>> myCaches = new EnumMap<>(CacheEnum.class); 058 059 public MemoryCacheService(JpaStorageSettings theStorageSettings) { 060 myStorageSettings = theStorageSettings; 061 062 populateCaches(); 063 } 064 065 private void populateCaches() { 066 for (CacheEnum next : CacheEnum.values()) { 067 068 long timeoutSeconds; 069 int maximumSize; 070 071 switch (next) { 072 case CONCEPT_TRANSLATION: 073 case CONCEPT_TRANSLATION_REVERSE: 074 timeoutSeconds = 075 SECONDS.convert(myStorageSettings.getTranslationCachesExpireAfterWriteInMinutes(), MINUTES); 076 maximumSize = 500000; 077 break; 078 case PID_TO_FORCED_ID: 079 case FORCED_ID_TO_PID: 080 case MATCH_URL: 081 case RESOURCE_LOOKUP_BY_FORCED_ID: 082 case HISTORY_COUNT: 083 case TAG_DEFINITION: 084 case RESOURCE_CONDITIONAL_CREATE_VERSION: 085 case FHIRPATH_EXPRESSION: 086 default: 087 timeoutSeconds = SECONDS.convert(1, MINUTES); 088 maximumSize = 10000; 089 if (myStorageSettings.isMassIngestionMode()) { 090 timeoutSeconds = SECONDS.convert(50, MINUTES); 091 maximumSize = 100000; 092 } 093 break; 094 } 095 096 Cache<Object, Object> nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize); 097 098 myCaches.put(next, nextCache); 099 } 100 } 101 102 public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 103 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 104 return doGet(theCache, theKey, theSupplier); 105 } 106 107 protected <K, T> T doGet(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 108 Cache<K, T> cache = getCache(theCache); 109 return cache.get(theKey, theSupplier); 110 } 111 112 /** 113 * Fetch an item from the cache if it exists, and use the loading function to 114 * obtain it otherwise. 115 * <p> 116 * This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}. 117 */ 118 public <K, T> T getThenPutAfterCommit(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 119 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 120 T retVal = getIfPresent(theCache, theKey); 121 if (retVal == null) { 122 retVal = theSupplier.apply(theKey); 123 putAfterCommit(theCache, theKey, retVal); 124 } 125 return retVal; 126 } 127 128 public <K, V> V getIfPresent(CacheEnum theCache, K theKey) { 129 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 130 return doGetIfPresent(theCache, theKey); 131 } 132 133 protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) { 134 return (V) getCache(theCache).getIfPresent(theKey); 135 } 136 137 public <K, V> void put(CacheEnum theCache, K theKey, V theValue) { 138 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()) 139 : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache " 140 + theCache; 141 doPut(theCache, theKey, theValue); 142 } 143 144 protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) { 145 getCache(theCache).put(theKey, theValue); 146 } 147 148 /** 149 * This method registers a transaction synchronization that puts an entry in the cache 150 * if and when the current database transaction successfully commits. If the 151 * transaction is rolled back, the key+value passed into this method will 152 * not be added to the cache. 153 * <p> 154 * This is useful for situations where you want to store something that has been 155 * resolved in the DB during the current transaction, but it's not yet guaranteed 156 * that this item will successfully save to the DB. Use this method in that case 157 * in order to avoid cache poisoning. 158 */ 159 public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) { 160 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()) 161 : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache " 162 + theCache; 163 if (TransactionSynchronizationManager.isSynchronizationActive()) { 164 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 165 @Override 166 public void afterCommit() { 167 put(theCache, theKey, theValue); 168 } 169 }); 170 } else { 171 put(theCache, theKey, theValue); 172 } 173 } 174 175 @SuppressWarnings("unchecked") 176 public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) { 177 return doGetAllPresent(theCache, theKeys); 178 } 179 180 @SuppressWarnings("unchecked") 181 protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) { 182 return (Map<K, V>) getCache(theCache).getAllPresent(theKeys); 183 } 184 185 public void invalidateAllCaches() { 186 myCaches.values().forEach(Cache::invalidateAll); 187 } 188 189 private <K, T> Cache<K, T> getCache(CacheEnum theCache) { 190 return (Cache<K, T>) myCaches.get(theCache); 191 } 192 193 public long getEstimatedSize(CacheEnum theCache) { 194 return getCache(theCache).estimatedSize(); 195 } 196 197 public void invalidateCaches(CacheEnum... theCaches) { 198 for (CacheEnum next : theCaches) { 199 getCache(next).invalidateAll(); 200 } 201 } 202 203 public enum CacheEnum { 204 TAG_DEFINITION(TagDefinitionCacheKey.class), 205 /** 206 * Key type: {@link ForcedIdCacheKey} 207 * Value type: {@literal JpaResourceLookup} 208 */ 209 RESOURCE_LOOKUP_BY_FORCED_ID(ForcedIdCacheKey.class), 210 FORCED_ID_TO_PID(String.class), 211 FHIRPATH_EXPRESSION(String.class), 212 /** 213 * Key type: {@literal Long} 214 * Value type: {@literal Optional<String>} 215 */ 216 PID_TO_FORCED_ID(Long.class), 217 /** 218 * TODO: JA this is duplicate with the CachingValidationSupport cache. 219 * A better solution would be to drop this cache for this item, and to 220 * create a new CachingValidationSupport implementation which uses 221 * the MemoryCacheService for all of its caches. 222 */ 223 CONCEPT_TRANSLATION(TranslationQuery.class), 224 MATCH_URL(String.class), 225 CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class), 226 RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class), 227 HISTORY_COUNT(HistoryCountKey.class), 228 NAME_TO_PARTITION(String.class), 229 ID_TO_PARTITION(Integer.class); 230 231 public Class<?> getKeyType() { 232 return myKeyType; 233 } 234 235 private final Class<?> myKeyType; 236 237 CacheEnum(Class<?> theKeyType) { 238 myKeyType = theKeyType; 239 } 240 } 241 242 public static class TagDefinitionCacheKey { 243 244 private final TagTypeEnum myType; 245 private final String mySystem; 246 private final String myCode; 247 private final String myVersion; 248 private Boolean myUserSelected; 249 private final int myHashCode; 250 251 public TagDefinitionCacheKey( 252 TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) { 253 myType = theType; 254 mySystem = theSystem; 255 myCode = theCode; 256 myVersion = theVersion; 257 myUserSelected = theUserSelected; 258 myHashCode = new HashCodeBuilder(17, 37) 259 .append(myType) 260 .append(mySystem) 261 .append(myCode) 262 .append(myVersion) 263 .append(myUserSelected) 264 .toHashCode(); 265 } 266 267 @Override 268 public boolean equals(Object theO) { 269 boolean retVal = false; 270 if (theO instanceof TagDefinitionCacheKey) { 271 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 272 273 retVal = new EqualsBuilder() 274 .append(myType, that.myType) 275 .append(mySystem, that.mySystem) 276 .append(myCode, that.myCode) 277 .isEquals(); 278 } 279 return retVal; 280 } 281 282 @Override 283 public int hashCode() { 284 return myHashCode; 285 } 286 } 287 288 public static class HistoryCountKey { 289 private final String myTypeName; 290 private final Long myInstanceId; 291 private final int myHashCode; 292 293 private HistoryCountKey(String theTypeName, Long theInstanceId) { 294 myTypeName = theTypeName; 295 myInstanceId = theInstanceId; 296 myHashCode = new HashCodeBuilder() 297 .append(myTypeName) 298 .append(myInstanceId) 299 .toHashCode(); 300 } 301 302 public static HistoryCountKey forSystem() { 303 return new HistoryCountKey(null, null); 304 } 305 306 public static HistoryCountKey forType(@Nonnull String theType) { 307 assert isNotBlank(theType); 308 return new HistoryCountKey(theType, null); 309 } 310 311 public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) { 312 assert theInstanceId != null; 313 return new HistoryCountKey(null, theInstanceId); 314 } 315 316 @Override 317 public boolean equals(Object theO) { 318 boolean retVal = false; 319 if (theO instanceof HistoryCountKey) { 320 HistoryCountKey that = (HistoryCountKey) theO; 321 retVal = new EqualsBuilder() 322 .append(myTypeName, that.myTypeName) 323 .append(myInstanceId, that.myInstanceId) 324 .isEquals(); 325 } 326 return retVal; 327 } 328 329 @Override 330 public int hashCode() { 331 return myHashCode; 332 } 333 } 334 335 public static class ForcedIdCacheKey { 336 337 private final String myResourceType; 338 private final String myResourceId; 339 private final RequestPartitionId myRequestPartitionId; 340 private final int myHashCode; 341 342 public ForcedIdCacheKey( 343 @Nullable String theResourceType, 344 @Nonnull String theResourceId, 345 @Nonnull RequestPartitionId theRequestPartitionId) { 346 myResourceType = theResourceType; 347 myResourceId = theResourceId; 348 myRequestPartitionId = theRequestPartitionId; 349 myHashCode = Objects.hash(myResourceType, myResourceId, myRequestPartitionId); 350 } 351 352 @Override 353 public boolean equals(Object theO) { 354 if (this == theO) { 355 return true; 356 } 357 if (!(theO instanceof ForcedIdCacheKey)) { 358 return false; 359 } 360 ForcedIdCacheKey that = (ForcedIdCacheKey) theO; 361 return Objects.equals(myResourceType, that.myResourceType) 362 && Objects.equals(myResourceId, that.myResourceId) 363 && Objects.equals(myRequestPartitionId, that.myRequestPartitionId); 364 } 365 366 @Override 367 public int hashCode() { 368 return myHashCode; 369 } 370 371 /** 372 * Creates and returns a new unqualified versionless IIdType instance 373 */ 374 public IIdType toIdType(FhirContext theFhirCtx) { 375 if (myResourceType == null) { 376 return toIdTypeWithoutResourceType(theFhirCtx); 377 } 378 IIdType retVal = theFhirCtx.getVersion().newIdType(); 379 retVal.setValue(myResourceType + "/" + myResourceId); 380 return retVal; 381 } 382 383 /** 384 * Creates and returns a new unqualified versionless IIdType instance 385 */ 386 public IIdType toIdTypeWithoutResourceType(FhirContext theFhirCtx) { 387 IIdType retVal = theFhirCtx.getVersion().newIdType(); 388 retVal.setValue(myResourceId); 389 return retVal; 390 } 391 } 392}