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.jpa.api.config.JpaStorageSettings; 023import ca.uhn.fhir.jpa.api.model.TranslationQuery; 024import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 025import ca.uhn.fhir.sl.cache.Cache; 026import ca.uhn.fhir.sl.cache.CacheFactory; 027import jakarta.annotation.Nonnull; 028import org.apache.commons.lang3.builder.EqualsBuilder; 029import org.apache.commons.lang3.builder.HashCodeBuilder; 030import org.springframework.transaction.support.TransactionSynchronization; 031import org.springframework.transaction.support.TransactionSynchronizationManager; 032 033import java.util.Collection; 034import java.util.EnumMap; 035import java.util.Map; 036import java.util.function.Function; 037 038import static java.util.concurrent.TimeUnit.MINUTES; 039import static java.util.concurrent.TimeUnit.SECONDS; 040import static org.apache.commons.lang3.StringUtils.isNotBlank; 041 042/** 043 * This class acts as a central spot for all of the many Caffeine caches we use in HAPI FHIR. 044 * <p> 045 * The API is super simplistic, and caches are all 1-minute, max 10000 entries for starters. We could definitely add nuance to this, 046 * which will be much easier now that this is being centralized. Some logging/monitoring would be good too. 047 */ 048// TODO: JA2 extract an interface for this class and use it everywhere 049public class MemoryCacheService { 050 051 private final JpaStorageSettings myStorageSettings; 052 private final EnumMap<CacheEnum, Cache<?, ?>> myCaches = new EnumMap<>(CacheEnum.class); 053 054 public MemoryCacheService(JpaStorageSettings theStorageSettings) { 055 myStorageSettings = theStorageSettings; 056 057 populateCaches(); 058 } 059 060 private void populateCaches() { 061 for (CacheEnum next : CacheEnum.values()) { 062 063 long timeoutSeconds; 064 int maximumSize; 065 066 switch (next) { 067 case CONCEPT_TRANSLATION: 068 case CONCEPT_TRANSLATION_REVERSE: 069 timeoutSeconds = 070 SECONDS.convert(myStorageSettings.getTranslationCachesExpireAfterWriteInMinutes(), MINUTES); 071 maximumSize = 500000; 072 break; 073 case PID_TO_FORCED_ID: 074 case FORCED_ID_TO_PID: 075 case MATCH_URL: 076 case RESOURCE_LOOKUP: 077 case HISTORY_COUNT: 078 case TAG_DEFINITION: 079 case RESOURCE_CONDITIONAL_CREATE_VERSION: 080 case FHIRPATH_EXPRESSION: 081 default: 082 timeoutSeconds = SECONDS.convert(1, MINUTES); 083 maximumSize = 10000; 084 if (myStorageSettings.isMassIngestionMode()) { 085 timeoutSeconds = SECONDS.convert(50, MINUTES); 086 maximumSize = 100000; 087 } 088 break; 089 } 090 091 Cache<Object, Object> nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize); 092 093 myCaches.put(next, nextCache); 094 } 095 } 096 097 public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 098 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 099 return doGet(theCache, theKey, theSupplier); 100 } 101 102 protected <K, T> T doGet(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 103 Cache<K, T> cache = getCache(theCache); 104 return cache.get(theKey, theSupplier); 105 } 106 107 /** 108 * Fetch an item from the cache if it exists, and use the loading function to 109 * obtain it otherwise. 110 * <p> 111 * This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}. 112 */ 113 public <K, T> T getThenPutAfterCommit(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 114 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 115 T retVal = getIfPresent(theCache, theKey); 116 if (retVal == null) { 117 retVal = theSupplier.apply(theKey); 118 putAfterCommit(theCache, theKey, retVal); 119 } 120 return retVal; 121 } 122 123 public <K, V> V getIfPresent(CacheEnum theCache, K theKey) { 124 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 125 return doGetIfPresent(theCache, theKey); 126 } 127 128 protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) { 129 return (V) getCache(theCache).getIfPresent(theKey); 130 } 131 132 public <K, V> void put(CacheEnum theCache, K theKey, V theValue) { 133 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 134 doPut(theCache, theKey, theValue); 135 } 136 137 protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) { 138 getCache(theCache).put(theKey, theValue); 139 } 140 141 /** 142 * This method registers a transaction synchronization that puts an entry in the cache 143 * if and when the current database transaction successfully commits. If the 144 * transaction is rolled back, the key+value passed into this method will 145 * not be added to the cache. 146 * <p> 147 * This is useful for situations where you want to store something that has been 148 * resolved in the DB during the current transaction, but it's not yet guaranteed 149 * that this item will successfully save to the DB. Use this method in that case 150 * in order to avoid cache poisoning. 151 */ 152 public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) { 153 if (TransactionSynchronizationManager.isSynchronizationActive()) { 154 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 155 @Override 156 public void afterCommit() { 157 put(theCache, theKey, theValue); 158 } 159 }); 160 } else { 161 put(theCache, theKey, theValue); 162 } 163 } 164 165 @SuppressWarnings("unchecked") 166 public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) { 167 return doGetAllPresent(theCache, theKeys); 168 } 169 170 @SuppressWarnings("unchecked") 171 protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) { 172 return (Map<K, V>) getCache(theCache).getAllPresent(theKeys); 173 } 174 175 public void invalidateAllCaches() { 176 myCaches.values().forEach(Cache::invalidateAll); 177 } 178 179 private <K, T> Cache<K, T> getCache(CacheEnum theCache) { 180 return (Cache<K, T>) myCaches.get(theCache); 181 } 182 183 public long getEstimatedSize(CacheEnum theCache) { 184 return getCache(theCache).estimatedSize(); 185 } 186 187 public void invalidateCaches(CacheEnum... theCaches) { 188 for (CacheEnum next : theCaches) { 189 getCache(next).invalidateAll(); 190 } 191 } 192 193 public enum CacheEnum { 194 TAG_DEFINITION(TagDefinitionCacheKey.class), 195 RESOURCE_LOOKUP(String.class), 196 FORCED_ID_TO_PID(String.class), 197 FHIRPATH_EXPRESSION(String.class), 198 /** 199 * Key type: {@literal Long} 200 * Value type: {@literal Optional<String>} 201 */ 202 PID_TO_FORCED_ID(Long.class), 203 /** 204 * TODO: JA this is duplicate with the CachingValidationSupport cache. 205 * A better solution would be to drop this cache for this item, and to 206 * create a new CachingValidationSupport implementation which uses 207 * the MemoryCacheService for all of its caches. 208 */ 209 CONCEPT_TRANSLATION(TranslationQuery.class), 210 MATCH_URL(String.class), 211 CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class), 212 RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class), 213 HISTORY_COUNT(HistoryCountKey.class), 214 NAME_TO_PARTITION(String.class), 215 ID_TO_PARTITION(Integer.class); 216 217 public Class<?> getKeyType() { 218 return myKeyType; 219 } 220 221 private final Class<?> myKeyType; 222 223 CacheEnum(Class<?> theKeyType) { 224 myKeyType = theKeyType; 225 } 226 } 227 228 public static class TagDefinitionCacheKey { 229 230 private final TagTypeEnum myType; 231 private final String mySystem; 232 private final String myCode; 233 private final String myVersion; 234 private Boolean myUserSelected; 235 private final int myHashCode; 236 237 public TagDefinitionCacheKey( 238 TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) { 239 myType = theType; 240 mySystem = theSystem; 241 myCode = theCode; 242 myVersion = theVersion; 243 myUserSelected = theUserSelected; 244 myHashCode = new HashCodeBuilder(17, 37) 245 .append(myType) 246 .append(mySystem) 247 .append(myCode) 248 .append(myVersion) 249 .append(myUserSelected) 250 .toHashCode(); 251 } 252 253 @Override 254 public boolean equals(Object theO) { 255 boolean retVal = false; 256 if (theO instanceof TagDefinitionCacheKey) { 257 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 258 259 retVal = new EqualsBuilder() 260 .append(myType, that.myType) 261 .append(mySystem, that.mySystem) 262 .append(myCode, that.myCode) 263 .isEquals(); 264 } 265 return retVal; 266 } 267 268 @Override 269 public int hashCode() { 270 return myHashCode; 271 } 272 } 273 274 public static class HistoryCountKey { 275 private final String myTypeName; 276 private final Long myInstanceId; 277 private final int myHashCode; 278 279 private HistoryCountKey(String theTypeName, Long theInstanceId) { 280 myTypeName = theTypeName; 281 myInstanceId = theInstanceId; 282 myHashCode = new HashCodeBuilder() 283 .append(myTypeName) 284 .append(myInstanceId) 285 .toHashCode(); 286 } 287 288 public static HistoryCountKey forSystem() { 289 return new HistoryCountKey(null, null); 290 } 291 292 public static HistoryCountKey forType(@Nonnull String theType) { 293 assert isNotBlank(theType); 294 return new HistoryCountKey(theType, null); 295 } 296 297 public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) { 298 assert theInstanceId != null; 299 return new HistoryCountKey(null, theInstanceId); 300 } 301 302 @Override 303 public boolean equals(Object theO) { 304 boolean retVal = false; 305 if (theO instanceof HistoryCountKey) { 306 HistoryCountKey that = (HistoryCountKey) theO; 307 retVal = new EqualsBuilder() 308 .append(myTypeName, that.myTypeName) 309 .append(myInstanceId, that.myInstanceId) 310 .isEquals(); 311 } 312 return retVal; 313 } 314 315 @Override 316 public int hashCode() { 317 return myHashCode; 318 } 319 } 320}