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}