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}