
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 public <K, V> V getIfPresent(CacheEnum theCache, K theKey) { 137 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 138 return doGetIfPresent(theCache, theKey); 139 } 140 141 protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) { 142 return (V) getCache(theCache).getIfPresent(theKey); 143 } 144 145 public <K, V> void put(CacheEnum theCache, K theKey, V theValue) { 146 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()) 147 : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache " 148 + theCache; 149 doPut(theCache, theKey, theValue); 150 } 151 152 protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) { 153 getCache(theCache).put(theKey, theValue); 154 } 155 156 /** 157 * This method registers a transaction synchronization that puts an entry in the cache 158 * if and when the current database transaction successfully commits. If the 159 * transaction is rolled back, the key+value passed into this method will 160 * not be added to the cache. 161 * <p> 162 * This is useful for situations where you want to store something that has been 163 * resolved in the DB during the current transaction, but it's not yet guaranteed 164 * that this item will successfully save to the DB. Use this method in that case 165 * in order to avoid cache poisoning. 166 */ 167 public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) { 168 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()) 169 : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache " 170 + theCache; 171 if (TransactionSynchronizationManager.isSynchronizationActive()) { 172 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 173 @Override 174 public void afterCommit() { 175 put(theCache, theKey, theValue); 176 } 177 }); 178 } else { 179 put(theCache, theKey, theValue); 180 } 181 } 182 183 @SuppressWarnings("unchecked") 184 public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) { 185 return doGetAllPresent(theCache, theKeys); 186 } 187 188 @SuppressWarnings("unchecked") 189 protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) { 190 return (Map<K, V>) getCache(theCache).getAllPresent(theKeys); 191 } 192 193 public void invalidateAllCaches() { 194 myCaches.values().forEach(Cache::invalidateAll); 195 } 196 197 private <K, T> Cache<K, T> getCache(CacheEnum theCache) { 198 return (Cache<K, T>) myCaches.get(theCache); 199 } 200 201 public long getEstimatedSize(CacheEnum theCache) { 202 return getCache(theCache).estimatedSize(); 203 } 204 205 public void invalidateCaches(CacheEnum... theCaches) { 206 for (CacheEnum next : theCaches) { 207 getCache(next).invalidateAll(); 208 } 209 } 210 211 public enum CacheEnum { 212 TAG_DEFINITION(TagDefinitionCacheKey.class), 213 /** 214 * Key type: {@link ForcedIdCacheKey} 215 * Value type: {@literal JpaResourceLookup} 216 */ 217 RESOURCE_LOOKUP_BY_FORCED_ID(ForcedIdCacheKey.class), 218 FHIRPATH_EXPRESSION(String.class), 219 /** 220 * Key type: {@literal Long} 221 * Value type: {@literal Optional<String>} 222 */ 223 PID_TO_FORCED_ID(JpaPid.class), 224 MATCH_URL(String.class), 225 RESOURCE_CONDITIONAL_CREATE_VERSION(JpaPid.class), 226 HISTORY_COUNT(HistoryCountKey.class), 227 NAME_TO_PARTITION(String.class), 228 ID_TO_PARTITION(Integer.class), 229 HASH_IDENTITY_TO_SEARCH_PARAM_IDENTITY(Long.class), 230 RES_TYPE_TO_RES_TYPE_ID(String.class), 231 RESOURCE_IDENTIFIER_SYSTEM_TO_PID(String.class), 232 PATIENT_IDENTIFIER_TO_FHIR_ID(IdentifierKey.class); 233 234 private final Class<?> myKeyType; 235 236 CacheEnum(Class<?> theKeyType) { 237 myKeyType = theKeyType; 238 } 239 240 public Class<?> getKeyType() { 241 return myKeyType; 242 } 243 } 244 245 public record IdentifierKey(String system, String value) {} 246 247 public static class TagDefinitionCacheKey { 248 249 private final TagTypeEnum myType; 250 private final String mySystem; 251 private final String myCode; 252 private final String myVersion; 253 private final int myHashCode; 254 private Boolean myUserSelected; 255 256 public TagDefinitionCacheKey( 257 TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) { 258 myType = theType; 259 mySystem = theSystem; 260 myCode = theCode; 261 myVersion = theVersion; 262 myUserSelected = theUserSelected; 263 myHashCode = new HashCodeBuilder(17, 37) 264 .append(myType) 265 .append(mySystem) 266 .append(myCode) 267 .append(myVersion) 268 .append(myUserSelected) 269 .toHashCode(); 270 } 271 272 @Override 273 public boolean equals(Object theO) { 274 boolean retVal = false; 275 if (theO instanceof TagDefinitionCacheKey) { 276 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 277 278 retVal = new EqualsBuilder() 279 .append(myType, that.myType) 280 .append(mySystem, that.mySystem) 281 .append(myCode, that.myCode) 282 .isEquals(); 283 } 284 return retVal; 285 } 286 287 @Override 288 public int hashCode() { 289 return myHashCode; 290 } 291 } 292 293 public static class HistoryCountKey { 294 private final String myTypeName; 295 private final Long myInstanceId; 296 private final Integer myPartitionId; 297 private final int myHashCode; 298 299 private HistoryCountKey(@Nullable String theTypeName, @Nullable JpaPid theInstanceId) { 300 myTypeName = theTypeName; 301 if (theInstanceId != null) { 302 myInstanceId = theInstanceId.getId(); 303 myPartitionId = theInstanceId.getPartitionId(); 304 } else { 305 myInstanceId = null; 306 myPartitionId = null; 307 } 308 myHashCode = new HashCodeBuilder() 309 .append(myTypeName) 310 .append(myInstanceId) 311 .append(myPartitionId) 312 .toHashCode(); 313 } 314 315 @Override 316 public boolean equals(Object theO) { 317 boolean retVal = false; 318 if (theO instanceof HistoryCountKey) { 319 HistoryCountKey that = (HistoryCountKey) theO; 320 retVal = new EqualsBuilder() 321 .append(myTypeName, that.myTypeName) 322 .append(myInstanceId, that.myInstanceId) 323 .isEquals(); 324 } 325 return retVal; 326 } 327 328 @Override 329 public int hashCode() { 330 return myHashCode; 331 } 332 333 public static HistoryCountKey forSystem() { 334 return new HistoryCountKey(null, null); 335 } 336 337 public static HistoryCountKey forType(@Nonnull String theType) { 338 assert isNotBlank(theType); 339 return new HistoryCountKey(theType, null); 340 } 341 342 public static HistoryCountKey forInstance(@Nonnull JpaPid theInstanceId) { 343 return new HistoryCountKey(null, theInstanceId); 344 } 345 } 346 347 public static class ForcedIdCacheKey { 348 349 private final String myResourceType; 350 private final String myResourceId; 351 private final List<Integer> myRequestPartitionIds; 352 private final int myHashCode; 353 354 public ForcedIdCacheKey( 355 @Nullable String theResourceType, 356 @Nonnull String theResourceId, 357 @Nonnull RequestPartitionId theRequestPartitionId) { 358 myResourceType = theResourceType; 359 myResourceId = theResourceId; 360 if (theRequestPartitionId.hasPartitionIds()) { 361 myRequestPartitionIds = theRequestPartitionId.getPartitionIds(); 362 } else { 363 myRequestPartitionIds = null; 364 } 365 myHashCode = Objects.hash(myResourceType, myResourceId, myRequestPartitionIds); 366 } 367 368 @Override 369 public String toString() { 370 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 371 .append("resType", myResourceType) 372 .append("resId", myResourceId) 373 .append("partId", myRequestPartitionIds) 374 .toString(); 375 } 376 377 @Override 378 public boolean equals(Object theO) { 379 if (this == theO) { 380 return true; 381 } 382 if (!(theO instanceof ForcedIdCacheKey)) { 383 return false; 384 } 385 ForcedIdCacheKey that = (ForcedIdCacheKey) theO; 386 return Objects.equals(myResourceType, that.myResourceType) 387 && Objects.equals(myResourceId, that.myResourceId) 388 && Objects.equals(myRequestPartitionIds, that.myRequestPartitionIds); 389 } 390 391 @Override 392 public int hashCode() { 393 return myHashCode; 394 } 395 396 /** 397 * Creates and returns a new unqualified versionless IIdType instance 398 */ 399 public IIdType toIdType(FhirContext theFhirCtx) { 400 return theFhirCtx.getVersion().newIdType(myResourceType, myResourceId); 401 } 402 } 403}