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 = 10000; 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 CONCEPT_TRANSLATION(TranslationQuery.class), 204 MATCH_URL(String.class), 205 CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class), 206 RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class), 207 HISTORY_COUNT(HistoryCountKey.class), 208 NAME_TO_PARTITION(String.class), 209 ID_TO_PARTITION(Integer.class); 210 211 public Class<?> getKeyType() { 212 return myKeyType; 213 } 214 215 private final Class<?> myKeyType; 216 217 CacheEnum(Class<?> theKeyType) { 218 myKeyType = theKeyType; 219 } 220 } 221 222 public static class TagDefinitionCacheKey { 223 224 private final TagTypeEnum myType; 225 private final String mySystem; 226 private final String myCode; 227 private final String myVersion; 228 private Boolean myUserSelected; 229 private final int myHashCode; 230 231 public TagDefinitionCacheKey( 232 TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) { 233 myType = theType; 234 mySystem = theSystem; 235 myCode = theCode; 236 myVersion = theVersion; 237 myUserSelected = theUserSelected; 238 myHashCode = new HashCodeBuilder(17, 37) 239 .append(myType) 240 .append(mySystem) 241 .append(myCode) 242 .append(myVersion) 243 .append(myUserSelected) 244 .toHashCode(); 245 } 246 247 @Override 248 public boolean equals(Object theO) { 249 boolean retVal = false; 250 if (theO instanceof TagDefinitionCacheKey) { 251 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 252 253 retVal = new EqualsBuilder() 254 .append(myType, that.myType) 255 .append(mySystem, that.mySystem) 256 .append(myCode, that.myCode) 257 .isEquals(); 258 } 259 return retVal; 260 } 261 262 @Override 263 public int hashCode() { 264 return myHashCode; 265 } 266 } 267 268 public static class HistoryCountKey { 269 private final String myTypeName; 270 private final Long myInstanceId; 271 private final int myHashCode; 272 273 private HistoryCountKey(String theTypeName, Long theInstanceId) { 274 myTypeName = theTypeName; 275 myInstanceId = theInstanceId; 276 myHashCode = new HashCodeBuilder() 277 .append(myTypeName) 278 .append(myInstanceId) 279 .toHashCode(); 280 } 281 282 public static HistoryCountKey forSystem() { 283 return new HistoryCountKey(null, null); 284 } 285 286 public static HistoryCountKey forType(@Nonnull String theType) { 287 assert isNotBlank(theType); 288 return new HistoryCountKey(theType, null); 289 } 290 291 public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) { 292 assert theInstanceId != null; 293 return new HistoryCountKey(null, theInstanceId); 294 } 295 296 @Override 297 public boolean equals(Object theO) { 298 boolean retVal = false; 299 if (theO instanceof HistoryCountKey) { 300 HistoryCountKey that = (HistoryCountKey) theO; 301 retVal = new EqualsBuilder() 302 .append(myTypeName, that.myTypeName) 303 .append(myInstanceId, that.myInstanceId) 304 .isEquals(); 305 } 306 return retVal; 307 } 308 309 @Override 310 public int hashCode() { 311 return myHashCode; 312 } 313 } 314}