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