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