
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2025 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.model.dao.JpaPid; 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.apache.commons.lang3.builder.ToStringBuilder; 034import org.apache.commons.lang3.builder.ToStringStyle; 035import org.hl7.fhir.instance.model.api.IIdType; 036import org.springframework.transaction.support.TransactionSynchronization; 037import org.springframework.transaction.support.TransactionSynchronizationManager; 038 039import java.util.Collection; 040import java.util.EnumMap; 041import java.util.List; 042import java.util.Map; 043import java.util.Objects; 044import java.util.function.Function; 045 046import static java.util.concurrent.TimeUnit.MINUTES; 047import static java.util.concurrent.TimeUnit.SECONDS; 048import static org.apache.commons.lang3.StringUtils.isNotBlank; 049 050/** 051 * This class acts as a central spot for all of the many Caffeine caches we use in HAPI FHIR. 052 * <p> 053 * The API is super simplistic, and caches are all 1-minute, max 10000 entries for starters. We could definitely add nuance to this, 054 * which will be much easier now that this is being centralized. Some logging/monitoring would be good too. 055 */ 056// TODO: JA2 extract an interface for this class and use it everywhere 057public class MemoryCacheService { 058 059 private final JpaStorageSettings myStorageSettings; 060 private final EnumMap<CacheEnum, Cache<?, ?>> myCaches = new EnumMap<>(CacheEnum.class); 061 062 public MemoryCacheService(JpaStorageSettings theStorageSettings) { 063 myStorageSettings = theStorageSettings; 064 065 populateCaches(); 066 } 067 068 private void populateCaches() { 069 for (CacheEnum next : CacheEnum.values()) { 070 071 long timeoutSeconds; 072 int maximumSize; 073 074 Cache<Object, Object> nextCache; 075 switch (next) { 076 case RES_TYPE_TO_RES_TYPE_ID: 077 nextCache = CacheFactory.buildEternal(250, 500); 078 break; 079 case HASH_IDENTITY_TO_SEARCH_PARAM_IDENTITY: 080 nextCache = CacheFactory.buildEternal(5_000, 50_000); 081 break; 082 case RESOURCE_IDENTIFIER_SYSTEM_TO_PID: 083 nextCache = CacheFactory.buildEternal(250, 1_000); 084 break; 085 case PATIENT_IDENTIFIER_TO_FHIR_ID: 086 case NAME_TO_PARTITION: 087 case ID_TO_PARTITION: 088 case PID_TO_FORCED_ID: 089 case MATCH_URL: 090 case RESOURCE_LOOKUP_BY_FORCED_ID: 091 case HISTORY_COUNT: 092 case TAG_DEFINITION: 093 case RESOURCE_CONDITIONAL_CREATE_VERSION: 094 case FHIRPATH_EXPRESSION: 095 default: 096 timeoutSeconds = SECONDS.convert(1, MINUTES); 097 maximumSize = 10000; 098 if (myStorageSettings.isMassIngestionMode()) { 099 timeoutSeconds = SECONDS.convert(50, MINUTES); 100 maximumSize = 100000; 101 } 102 nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize); 103 break; 104 } 105 106 myCaches.put(next, nextCache); 107 } 108 } 109 110 public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 111 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 112 return doGet(theCache, theKey, theSupplier); 113 } 114 115 protected <K, T> T doGet(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 116 Cache<K, T> cache = getCache(theCache); 117 return cache.get(theKey, theSupplier); 118 } 119 120 /** 121 * Fetch an item from the cache if it exists, and use the loading function to 122 * obtain it otherwise. 123 * <p> 124 * This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}. 125 */ 126 public <K, T> T getThenPutAfterCommit(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 127 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 128 T retVal = getIfPresent(theCache, theKey); 129 if (retVal == null) { 130 retVal = theSupplier.apply(theKey); 131 putAfterCommit(theCache, theKey, retVal); 132 } 133 return retVal; 134 } 135 136 /** 137 * Fetch an item from the cache if it exists and use the loading function to 138 * obtain it otherwise. If the loading function returns null, the item will not 139 * be placed in the cache and <code>null</code> will be returned. 140 * <p> 141 * This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}. 142 * 143 * @since 8.6.0 144 */ 145 public <K, T> T getThenPutAfterCommitIfNotNull(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 146 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 147 T retVal = getIfPresent(theCache, theKey); 148 if (retVal == null) { 149 retVal = theSupplier.apply(theKey); 150 if (retVal != null) { 151 putAfterCommit(theCache, theKey, retVal); 152 } 153 } 154 return retVal; 155 } 156 157 public <K, V> V getIfPresent(CacheEnum theCache, K theKey) { 158 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 159 return doGetIfPresent(theCache, theKey); 160 } 161 162 protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) { 163 return (V) getCache(theCache).getIfPresent(theKey); 164 } 165 166 public <K, V> void put(CacheEnum theCache, K theKey, V theValue) { 167 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()) 168 : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache " 169 + theCache; 170 doPut(theCache, theKey, theValue); 171 } 172 173 protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) { 174 getCache(theCache).put(theKey, theValue); 175 } 176 177 /** 178 * This method registers a transaction synchronization that puts an entry in the cache 179 * if and when the current database transaction successfully commits. If the 180 * transaction is rolled back, the key+value passed into this method will 181 * not be added to the cache. 182 * <p> 183 * This is useful for situations where you want to store something that has been 184 * resolved in the DB during the current transaction, but it's not yet guaranteed 185 * that this item will successfully save to the DB. Use this method in that case 186 * in order to avoid cache poisoning. 187 */ 188 public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) { 189 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()) 190 : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache " 191 + theCache; 192 if (TransactionSynchronizationManager.isSynchronizationActive()) { 193 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 194 @Override 195 public void afterCommit() { 196 put(theCache, theKey, theValue); 197 } 198 }); 199 } else { 200 put(theCache, theKey, theValue); 201 } 202 } 203 204 @SuppressWarnings("unchecked") 205 public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) { 206 return doGetAllPresent(theCache, theKeys); 207 } 208 209 @SuppressWarnings("unchecked") 210 protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) { 211 return (Map<K, V>) getCache(theCache).getAllPresent(theKeys); 212 } 213 214 public void invalidateAllCaches() { 215 myCaches.values().forEach(Cache::invalidateAll); 216 } 217 218 private <K, T> Cache<K, T> getCache(CacheEnum theCache) { 219 return (Cache<K, T>) myCaches.get(theCache); 220 } 221 222 public long getEstimatedSize(CacheEnum theCache) { 223 return getCache(theCache).estimatedSize(); 224 } 225 226 public void invalidateCaches(CacheEnum... theCaches) { 227 for (CacheEnum next : theCaches) { 228 getCache(next).invalidateAll(); 229 } 230 } 231 232 public enum CacheEnum { 233 TAG_DEFINITION(TagDefinitionCacheKey.class), 234 /** 235 * Key type: {@link ForcedIdCacheKey} 236 * Value type: {@literal JpaResourceLookup} 237 */ 238 RESOURCE_LOOKUP_BY_FORCED_ID(ForcedIdCacheKey.class), 239 FHIRPATH_EXPRESSION(String.class), 240 /** 241 * Key type: {@literal Long} 242 * Value type: {@literal Optional<String>} 243 */ 244 PID_TO_FORCED_ID(JpaPid.class), 245 MATCH_URL(String.class), 246 RESOURCE_CONDITIONAL_CREATE_VERSION(JpaPid.class), 247 HISTORY_COUNT(HistoryCountKey.class), 248 NAME_TO_PARTITION(String.class), 249 ID_TO_PARTITION(Integer.class), 250 HASH_IDENTITY_TO_SEARCH_PARAM_IDENTITY(Long.class), 251 RES_TYPE_TO_RES_TYPE_ID(String.class), 252 RESOURCE_IDENTIFIER_SYSTEM_TO_PID(String.class), 253 PATIENT_IDENTIFIER_TO_FHIR_ID(IdentifierKey.class); 254 255 private final Class<?> myKeyType; 256 257 CacheEnum(Class<?> theKeyType) { 258 myKeyType = theKeyType; 259 } 260 261 public Class<?> getKeyType() { 262 return myKeyType; 263 } 264 } 265 266 public record IdentifierKey(String system, String value) {} 267 268 public static class TagDefinitionCacheKey { 269 270 private final TagTypeEnum myType; 271 private final String mySystem; 272 private final String myCode; 273 private final String myVersion; 274 private final int myHashCode; 275 private Boolean myUserSelected; 276 277 public TagDefinitionCacheKey( 278 TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) { 279 myType = theType; 280 mySystem = theSystem; 281 myCode = theCode; 282 myVersion = theVersion; 283 myUserSelected = theUserSelected; 284 myHashCode = new HashCodeBuilder(17, 37) 285 .append(myType) 286 .append(mySystem) 287 .append(myCode) 288 .append(myVersion) 289 .append(myUserSelected) 290 .toHashCode(); 291 } 292 293 @Override 294 public boolean equals(Object theO) { 295 boolean retVal = false; 296 if (theO instanceof TagDefinitionCacheKey) { 297 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 298 299 retVal = new EqualsBuilder() 300 .append(myType, that.myType) 301 .append(mySystem, that.mySystem) 302 .append(myCode, that.myCode) 303 .isEquals(); 304 } 305 return retVal; 306 } 307 308 @Override 309 public int hashCode() { 310 return myHashCode; 311 } 312 } 313 314 public static class HistoryCountKey { 315 private final String myTypeName; 316 private final Long myInstanceId; 317 private final Integer myPartitionId; 318 private final int myHashCode; 319 320 private HistoryCountKey(@Nullable String theTypeName, @Nullable JpaPid theInstanceId) { 321 myTypeName = theTypeName; 322 if (theInstanceId != null) { 323 myInstanceId = theInstanceId.getId(); 324 myPartitionId = theInstanceId.getPartitionId(); 325 } else { 326 myInstanceId = null; 327 myPartitionId = null; 328 } 329 myHashCode = new HashCodeBuilder() 330 .append(myTypeName) 331 .append(myInstanceId) 332 .append(myPartitionId) 333 .toHashCode(); 334 } 335 336 @Override 337 public boolean equals(Object theO) { 338 boolean retVal = false; 339 if (theO instanceof HistoryCountKey) { 340 HistoryCountKey that = (HistoryCountKey) theO; 341 retVal = new EqualsBuilder() 342 .append(myTypeName, that.myTypeName) 343 .append(myInstanceId, that.myInstanceId) 344 .isEquals(); 345 } 346 return retVal; 347 } 348 349 @Override 350 public int hashCode() { 351 return myHashCode; 352 } 353 354 public static HistoryCountKey forSystem() { 355 return new HistoryCountKey(null, null); 356 } 357 358 public static HistoryCountKey forType(@Nonnull String theType) { 359 assert isNotBlank(theType); 360 return new HistoryCountKey(theType, null); 361 } 362 363 public static HistoryCountKey forInstance(@Nonnull JpaPid theInstanceId) { 364 return new HistoryCountKey(null, theInstanceId); 365 } 366 } 367 368 public static class ForcedIdCacheKey { 369 370 private final String myResourceType; 371 private final String myResourceId; 372 private final List<Integer> myRequestPartitionIds; 373 private final int myHashCode; 374 375 public ForcedIdCacheKey( 376 @Nullable String theResourceType, 377 @Nonnull String theResourceId, 378 @Nonnull RequestPartitionId theRequestPartitionId) { 379 myResourceType = theResourceType; 380 myResourceId = theResourceId; 381 if (theRequestPartitionId.hasPartitionIds()) { 382 myRequestPartitionIds = theRequestPartitionId.getPartitionIds(); 383 } else { 384 myRequestPartitionIds = null; 385 } 386 myHashCode = Objects.hash(myResourceType, myResourceId, myRequestPartitionIds); 387 } 388 389 @Override 390 public String toString() { 391 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 392 .append("resType", myResourceType) 393 .append("resId", myResourceId) 394 .append("partId", myRequestPartitionIds) 395 .toString(); 396 } 397 398 @Override 399 public boolean equals(Object theO) { 400 if (this == theO) { 401 return true; 402 } 403 if (!(theO instanceof ForcedIdCacheKey)) { 404 return false; 405 } 406 ForcedIdCacheKey that = (ForcedIdCacheKey) theO; 407 return Objects.equals(myResourceType, that.myResourceType) 408 && Objects.equals(myResourceId, that.myResourceId) 409 && Objects.equals(myRequestPartitionIds, that.myRequestPartitionIds); 410 } 411 412 @Override 413 public int hashCode() { 414 return myHashCode; 415 } 416 417 /** 418 * Creates and returns a new unqualified versionless IIdType instance 419 */ 420 public IIdType toIdType(FhirContext theFhirCtx) { 421 return theFhirCtx.getVersion().newIdType(myResourceType, myResourceId); 422 } 423 } 424}