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}