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