001package ca.uhn.fhir.jpa.util; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.jpa.api.config.DaoConfig; 024import ca.uhn.fhir.jpa.api.model.TranslationQuery; 025import ca.uhn.fhir.jpa.model.entity.TagTypeEnum; 026import com.github.benmanes.caffeine.cache.Cache; 027import com.github.benmanes.caffeine.cache.Caffeine; 028import org.apache.commons.lang3.builder.EqualsBuilder; 029import org.apache.commons.lang3.builder.HashCodeBuilder; 030import org.springframework.beans.factory.annotation.Autowired; 031import org.springframework.transaction.support.TransactionSynchronization; 032import org.springframework.transaction.support.TransactionSynchronizationManager; 033 034import javax.annotation.Nonnull; 035import javax.annotation.PostConstruct; 036import java.util.EnumMap; 037import java.util.Map; 038import java.util.function.Function; 039 040import static java.util.concurrent.TimeUnit.MINUTES; 041import static java.util.concurrent.TimeUnit.SECONDS; 042import static org.apache.commons.lang3.StringUtils.isNotBlank; 043 044/** 045 * This class acts as a central spot for all of the many Caffeine caches we use in HAPI FHIR. 046 * <p> 047 * The API is super simplistic, and caches are all 1-minute, max 10000 entries for starters. We could definitely add nuance to this, 048 * which will be much easier now that this is being centralized. Some logging/monitoring would be good too. 049 */ 050public class MemoryCacheService { 051 052 @Autowired 053 DaoConfig myDaoConfig; 054 055 private EnumMap<CacheEnum, Cache<?, ?>> myCaches; 056 057 @PostConstruct 058 public void start() { 059 060 myCaches = new EnumMap<>(CacheEnum.class); 061 062 for (CacheEnum next : CacheEnum.values()) { 063 064 long timeoutSeconds; 065 int maximumSize; 066 067 switch (next) { 068 case CONCEPT_TRANSLATION: 069 case CONCEPT_TRANSLATION_REVERSE: 070 timeoutSeconds = SECONDS.convert(myDaoConfig.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 (myDaoConfig.isMassIngestionMode()) { 084 timeoutSeconds = SECONDS.convert(50, MINUTES); 085 maximumSize = 100000; 086 } 087 break; 088 } 089 090 Cache<Object, Object> nextCache = Caffeine.newBuilder() 091 .expireAfterWrite(timeoutSeconds, SECONDS) 092 .maximumSize(maximumSize) 093 .build(); 094 095 myCaches.put(next, nextCache); 096 } 097 098 } 099 100 101 public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) { 102 assert theCache.myKeyType.isAssignableFrom(theKey.getClass()); 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.myKeyType.isAssignableFrom(theKey.getClass()); 115 116 Cache<K, T> cache = getCache(theCache); 117 T retVal = cache.getIfPresent(theKey); 118 if (retVal == null) { 119 retVal = theSupplier.apply(theKey); 120 putAfterCommit(theCache, theKey, retVal); 121 } 122 return retVal; 123 } 124 125 public <K, V> V getIfPresent(CacheEnum theCache, K theKey) { 126 assert theCache.myKeyType.isAssignableFrom(theKey.getClass()); 127 return (V) getCache(theCache).getIfPresent(theKey); 128 } 129 130 public <K, V> void put(CacheEnum theCache, K theKey, V theValue) { 131 assert theCache.myKeyType.isAssignableFrom(theKey.getClass()); 132 getCache(theCache).put(theKey, theValue); 133 } 134 135 /** 136 * This method registers a transaction synchronization that puts an entry in the cache 137 * if and when the current database transaction successfully commits. If the 138 * transaction is rolled back, the key+value passed into this method will 139 * not be added to the cache. 140 * <p> 141 * This is useful for situations where you want to store something that has been 142 * resolved in the DB during the current transaction, but it's not yet guaranteed 143 * that this item will successfully save to the DB. Use this method in that case 144 * in order to avoid cache poisoning. 145 */ 146 public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) { 147 if (TransactionSynchronizationManager.isSynchronizationActive()) { 148 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { 149 @Override 150 public void afterCommit() { 151 put(theCache, theKey, theValue); 152 } 153 }); 154 } else { 155 put(theCache, theKey, theValue); 156 } 157 } 158 159 @SuppressWarnings("unchecked") 160 public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Iterable<K> theKeys) { 161 return (Map<K, V>) getCache(theCache).getAllPresent(theKeys); 162 } 163 164 public void invalidateAllCaches() { 165 myCaches.values().forEach(Cache::invalidateAll); 166 } 167 168 private <K, T> Cache<K, T> getCache(CacheEnum theCache) { 169 return (Cache<K, T>) myCaches.get(theCache); 170 } 171 172 public long getEstimatedSize(CacheEnum theCache) { 173 return getCache(theCache).estimatedSize(); 174 } 175 176 public enum CacheEnum { 177 178 TAG_DEFINITION(TagDefinitionCacheKey.class), 179 RESOURCE_LOOKUP(String.class), 180 FORCED_ID_TO_PID(String.class), 181 /** 182 * Key type: {@literal Long} 183 * Value type: {@literal Optional<String>} 184 */ 185 PID_TO_FORCED_ID(Long.class), 186 CONCEPT_TRANSLATION(TranslationQuery.class), 187 MATCH_URL(String.class), 188 CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class), 189 RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class), 190 HISTORY_COUNT(HistoryCountKey.class); 191 192 private final Class<?> myKeyType; 193 194 CacheEnum(Class<?> theKeyType) { 195 myKeyType = theKeyType; 196 } 197 } 198 199 200 public static class TagDefinitionCacheKey { 201 202 private final TagTypeEnum myType; 203 private final String mySystem; 204 private final String myCode; 205 private final int myHashCode; 206 207 public TagDefinitionCacheKey(TagTypeEnum theType, String theSystem, String theCode) { 208 myType = theType; 209 mySystem = theSystem; 210 myCode = theCode; 211 myHashCode = new HashCodeBuilder(17, 37) 212 .append(myType) 213 .append(mySystem) 214 .append(myCode) 215 .toHashCode(); 216 } 217 218 @Override 219 public boolean equals(Object theO) { 220 boolean retVal = false; 221 if (theO instanceof TagDefinitionCacheKey) { 222 TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO; 223 224 retVal = new EqualsBuilder() 225 .append(myType, that.myType) 226 .append(mySystem, that.mySystem) 227 .append(myCode, that.myCode) 228 .isEquals(); 229 } 230 return retVal; 231 } 232 233 @Override 234 public int hashCode() { 235 return myHashCode; 236 } 237 } 238 239 240 public static class HistoryCountKey { 241 private final String myTypeName; 242 private final Long myInstanceId; 243 private final int myHashCode; 244 245 private HistoryCountKey(String theTypeName, Long theInstanceId) { 246 myTypeName = theTypeName; 247 myInstanceId = theInstanceId; 248 myHashCode = new HashCodeBuilder().append(myTypeName).append(myInstanceId).toHashCode(); 249 } 250 251 public static HistoryCountKey forSystem() { 252 return new HistoryCountKey(null, null); 253 } 254 255 public static HistoryCountKey forType(@Nonnull String theType) { 256 assert isNotBlank(theType); 257 return new HistoryCountKey(theType, null); 258 } 259 260 public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) { 261 assert theInstanceId != null; 262 return new HistoryCountKey(null, theInstanceId); 263 } 264 265 @Override 266 public boolean equals(Object theO) { 267 boolean retVal = false; 268 if (theO instanceof HistoryCountKey) { 269 HistoryCountKey that = (HistoryCountKey) theO; 270 retVal = new EqualsBuilder().append(myTypeName, that.myTypeName).append(myInstanceId, that.myInstanceId).isEquals(); 271 } 272 return retVal; 273 } 274 275 @Override 276 public int hashCode() { 277 return myHashCode; 278 } 279 280 } 281 282}