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