
001/*- 002 * #%L 003 * HAPI FHIR Storage api 004 * %% 005 * Copyright (C) 2014 - 2023 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 org.apache.commons.lang3.builder.EqualsBuilder; 028import org.apache.commons.lang3.builder.HashCodeBuilder; 029import org.springframework.transaction.support.TransactionSynchronization; 030import org.springframework.transaction.support.TransactionSynchronizationManager; 031 032import javax.annotation.Nonnull; 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 = SECONDS.convert(myStorageSettings.getTranslationCachesExpireAfterWriteInMinutes(), MINUTES); 070 maximumSize = 10000; 071 break; 072 case PID_TO_FORCED_ID: 073 case FORCED_ID_TO_PID: 074 case MATCH_URL: 075 case RESOURCE_LOOKUP: 076 case HISTORY_COUNT: 077 case TAG_DEFINITION: 078 case RESOURCE_CONDITIONAL_CREATE_VERSION: 079 default: 080 timeoutSeconds = SECONDS.convert(1, MINUTES); 081 maximumSize = 10000; 082 if (myStorageSettings.isMassIngestionMode()) { 083 timeoutSeconds = SECONDS.convert(50, MINUTES); 084 maximumSize = 100000; 085 } 086 break; 087 } 088 089 Cache<Object, Object> nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize); 090 091 myCaches.put(next, nextCache); 092 } 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 enum CacheEnum { 187 188 TAG_DEFINITION(TagDefinitionCacheKey.class), 189 RESOURCE_LOOKUP(String.class), 190 FORCED_ID_TO_PID(String.class), 191 /** 192 * Key type: {@literal Long} 193 * Value type: {@literal Optional<String>} 194 */ 195 PID_TO_FORCED_ID(Long.class), 196 CONCEPT_TRANSLATION(TranslationQuery.class), 197 MATCH_URL(String.class), 198 CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class), 199 RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class), 200 HISTORY_COUNT(HistoryCountKey.class); 201 202 public Class<?> getKeyType() { 203 return myKeyType; 204 } 205 206 private final Class<?> myKeyType; 207 208 CacheEnum(Class<?> theKeyType) { 209 myKeyType = theKeyType; 210 } 211 } 212 213 214 public static class TagDefinitionCacheKey { 215 216 private final TagTypeEnum myType; 217 private final String mySystem; 218 private final String myCode; 219 private final String myVersion; 220 private Boolean myUserSelected; 221 private final int myHashCode; 222 223 public TagDefinitionCacheKey(TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) { 224 myType = theType; 225 mySystem = theSystem; 226 myCode = theCode; 227 myVersion = theVersion; 228 myUserSelected = theUserSelected; 229 myHashCode = new HashCodeBuilder(17, 37) 230 .append(myType) 231 .append(mySystem) 232 .append(myCode) 233 .append(myVersion) 234 .append(myUserSelected) 235 .toHashCode(); 236 } 237 238 @Override 239 public boolean equals(Object theO) { 240 boolean retVal = false; 241 if (theO instanceof TagDefinitionCacheKey) { 242 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 243 244 retVal = new EqualsBuilder() 245 .append(myType, that.myType) 246 .append(mySystem, that.mySystem) 247 .append(myCode, that.myCode) 248 .isEquals(); 249 } 250 return retVal; 251 } 252 253 @Override 254 public int hashCode() { 255 return myHashCode; 256 } 257 } 258 259 260 public static class HistoryCountKey { 261 private final String myTypeName; 262 private final Long myInstanceId; 263 private final int myHashCode; 264 265 private HistoryCountKey(String theTypeName, Long theInstanceId) { 266 myTypeName = theTypeName; 267 myInstanceId = theInstanceId; 268 myHashCode = new HashCodeBuilder().append(myTypeName).append(myInstanceId).toHashCode(); 269 } 270 271 public static HistoryCountKey forSystem() { 272 return new HistoryCountKey(null, null); 273 } 274 275 public static HistoryCountKey forType(@Nonnull String theType) { 276 assert isNotBlank(theType); 277 return new HistoryCountKey(theType, null); 278 } 279 280 public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) { 281 assert theInstanceId != null; 282 return new HistoryCountKey(null, theInstanceId); 283 } 284 285 @Override 286 public boolean equals(Object theO) { 287 boolean retVal = false; 288 if (theO instanceof HistoryCountKey) { 289 HistoryCountKey that = (HistoryCountKey) theO; 290 retVal = new EqualsBuilder().append(myTypeName, that.myTypeName).append(myInstanceId, that.myInstanceId).isEquals(); 291 } 292 return retVal; 293 } 294 295 @Override 296 public int hashCode() { 297 return myHashCode; 298 } 299 300 } 301 302}