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 default: 081 timeoutSeconds = SECONDS.convert(1, MINUTES); 082 maximumSize = 10000; 083 if (myStorageSettings.isMassIngestionMode()) { 084 timeoutSeconds = SECONDS.convert(50, MINUTES); 085 maximumSize = 100000; 086 } 087 break; 088 } 089 090 Cache<Object, Object> nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize); 091 092 myCaches.put(next, nextCache); 093 } 094 } 095 096 public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 097 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 098 return doGet(theCache, theKey, theSupplier); 099 } 100 101 protected <K, T> T doGet(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 102 Cache<K, T> cache = getCache(theCache); 103 return cache.get(theKey, theSupplier); 104 } 105 106 /** 107 * Fetch an item from the cache if it exists, and use the loading function to 108 * obtain it otherwise. 109 * <p> 110 * This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}. 111 */ 112 public <K, T> T getThenPutAfterCommit(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 113 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 114 T retVal = getIfPresent(theCache, theKey); 115 if (retVal == null) { 116 retVal = theSupplier.apply(theKey); 117 putAfterCommit(theCache, theKey, retVal); 118 } 119 return retVal; 120 } 121 122 public <K, V> V getIfPresent(CacheEnum theCache, K theKey) { 123 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 124 return doGetIfPresent(theCache, theKey); 125 } 126 127 protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) { 128 return (V) getCache(theCache).getIfPresent(theKey); 129 } 130 131 public <K, V> void put(CacheEnum theCache, K theKey, V theValue) { 132 assert theCache.getKeyType().isAssignableFrom(theKey.getClass()); 133 doPut(theCache, theKey, theValue); 134 } 135 136 protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) { 137 getCache(theCache).put(theKey, theValue); 138 } 139 140 /** 141 * This method registers a transaction synchronization that puts an entry in the cache 142 * if and when the current database transaction successfully commits. If the 143 * transaction is rolled back, the key+value passed into this method will 144 * not be added to the cache. 145 * <p> 146 * This is useful for situations where you want to store something that has been 147 * resolved in the DB during the current transaction, but it's not yet guaranteed 148 * that this item will successfully save to the DB. Use this method in that case 149 * in order to avoid cache poisoning. 150 */ 151 public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) { 152 if (TransactionSynchronizationManager.isSynchronizationActive()) { 153 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 154 @Override 155 public void afterCommit() { 156 put(theCache, theKey, theValue); 157 } 158 }); 159 } else { 160 put(theCache, theKey, theValue); 161 } 162 } 163 164 @SuppressWarnings("unchecked") 165 public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) { 166 return doGetAllPresent(theCache, theKeys); 167 } 168 169 @SuppressWarnings("unchecked") 170 protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) { 171 return (Map<K, V>) getCache(theCache).getAllPresent(theKeys); 172 } 173 174 public void invalidateAllCaches() { 175 myCaches.values().forEach(Cache::invalidateAll); 176 } 177 178 private <K, T> Cache<K, T> getCache(CacheEnum theCache) { 179 return (Cache<K, T>) myCaches.get(theCache); 180 } 181 182 public long getEstimatedSize(CacheEnum theCache) { 183 return getCache(theCache).estimatedSize(); 184 } 185 186 public void invalidateCaches(CacheEnum... theCaches) { 187 for (CacheEnum next : theCaches) { 188 getCache(next).invalidateAll(); 189 } 190 } 191 192 public enum CacheEnum { 193 TAG_DEFINITION(TagDefinitionCacheKey.class), 194 RESOURCE_LOOKUP(String.class), 195 FORCED_ID_TO_PID(String.class), 196 /** 197 * Key type: {@literal Long} 198 * Value type: {@literal Optional<String>} 199 */ 200 PID_TO_FORCED_ID(Long.class), 201 CONCEPT_TRANSLATION(TranslationQuery.class), 202 MATCH_URL(String.class), 203 CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class), 204 RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class), 205 HISTORY_COUNT(HistoryCountKey.class), 206 NAME_TO_PARTITION(String.class), 207 ID_TO_PARTITION(Integer.class); 208 209 public Class<?> getKeyType() { 210 return myKeyType; 211 } 212 213 private final Class<?> myKeyType; 214 215 CacheEnum(Class<?> theKeyType) { 216 myKeyType = theKeyType; 217 } 218 } 219 220 public static class TagDefinitionCacheKey { 221 222 private final TagTypeEnum myType; 223 private final String mySystem; 224 private final String myCode; 225 private final String myVersion; 226 private Boolean myUserSelected; 227 private final int myHashCode; 228 229 public TagDefinitionCacheKey( 230 TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) { 231 myType = theType; 232 mySystem = theSystem; 233 myCode = theCode; 234 myVersion = theVersion; 235 myUserSelected = theUserSelected; 236 myHashCode = new HashCodeBuilder(17, 37) 237 .append(myType) 238 .append(mySystem) 239 .append(myCode) 240 .append(myVersion) 241 .append(myUserSelected) 242 .toHashCode(); 243 } 244 245 @Override 246 public boolean equals(Object theO) { 247 boolean retVal = false; 248 if (theO instanceof TagDefinitionCacheKey) { 249 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 250 251 retVal = new EqualsBuilder() 252 .append(myType, that.myType) 253 .append(mySystem, that.mySystem) 254 .append(myCode, that.myCode) 255 .isEquals(); 256 } 257 return retVal; 258 } 259 260 @Override 261 public int hashCode() { 262 return myHashCode; 263 } 264 } 265 266 public static class HistoryCountKey { 267 private final String myTypeName; 268 private final Long myInstanceId; 269 private final int myHashCode; 270 271 private HistoryCountKey(String theTypeName, Long theInstanceId) { 272 myTypeName = theTypeName; 273 myInstanceId = theInstanceId; 274 myHashCode = new HashCodeBuilder() 275 .append(myTypeName) 276 .append(myInstanceId) 277 .toHashCode(); 278 } 279 280 public static HistoryCountKey forSystem() { 281 return new HistoryCountKey(null, null); 282 } 283 284 public static HistoryCountKey forType(@Nonnull String theType) { 285 assert isNotBlank(theType); 286 return new HistoryCountKey(theType, null); 287 } 288 289 public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) { 290 assert theInstanceId != null; 291 return new HistoryCountKey(null, theInstanceId); 292 } 293 294 @Override 295 public boolean equals(Object theO) { 296 boolean retVal = false; 297 if (theO instanceof HistoryCountKey) { 298 HistoryCountKey that = (HistoryCountKey) theO; 299 retVal = new EqualsBuilder() 300 .append(myTypeName, that.myTypeName) 301 .append(myInstanceId, that.myInstanceId) 302 .isEquals(); 303 } 304 return retVal; 305 } 306 307 @Override 308 public int hashCode() { 309 return myHashCode; 310 } 311 } 312}