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 = 500000;
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                                case FHIRPATH_EXPRESSION:
081                                default:
082                                        timeoutSeconds = SECONDS.convert(1, MINUTES);
083                                        maximumSize = 10000;
084                                        if (myStorageSettings.isMassIngestionMode()) {
085                                                timeoutSeconds = SECONDS.convert(50, MINUTES);
086                                                maximumSize = 100000;
087                                        }
088                                        break;
089                        }
090
091                        Cache<Object, Object> nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize);
092
093                        myCaches.put(next, nextCache);
094                }
095        }
096
097        public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
098                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
099                return doGet(theCache, theKey, theSupplier);
100        }
101
102        protected <K, T> T doGet(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
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.getKeyType().isAssignableFrom(theKey.getClass());
115                T retVal = getIfPresent(theCache, theKey);
116                if (retVal == null) {
117                        retVal = theSupplier.apply(theKey);
118                        putAfterCommit(theCache, theKey, retVal);
119                }
120                return retVal;
121        }
122
123        public <K, V> V getIfPresent(CacheEnum theCache, K theKey) {
124                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
125                return doGetIfPresent(theCache, theKey);
126        }
127
128        protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) {
129                return (V) getCache(theCache).getIfPresent(theKey);
130        }
131
132        public <K, V> void put(CacheEnum theCache, K theKey, V theValue) {
133                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
134                doPut(theCache, theKey, theValue);
135        }
136
137        protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) {
138                getCache(theCache).put(theKey, theValue);
139        }
140
141        /**
142         * This method registers a transaction synchronization that puts an entry in the cache
143         * if and when the current database transaction successfully commits. If the
144         * transaction is rolled back, the key+value passed into this method will
145         * not be added to the cache.
146         * <p>
147         * This is useful for situations where you want to store something that has been
148         * resolved in the DB during the current transaction, but it's not yet guaranteed
149         * that this item will successfully save to the DB. Use this method in that case
150         * in order to avoid cache poisoning.
151         */
152        public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) {
153                if (TransactionSynchronizationManager.isSynchronizationActive()) {
154                        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
155                                @Override
156                                public void afterCommit() {
157                                        put(theCache, theKey, theValue);
158                                }
159                        });
160                } else {
161                        put(theCache, theKey, theValue);
162                }
163        }
164
165        @SuppressWarnings("unchecked")
166        public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) {
167                return doGetAllPresent(theCache, theKeys);
168        }
169
170        @SuppressWarnings("unchecked")
171        protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) {
172                return (Map<K, V>) getCache(theCache).getAllPresent(theKeys);
173        }
174
175        public void invalidateAllCaches() {
176                myCaches.values().forEach(Cache::invalidateAll);
177        }
178
179        private <K, T> Cache<K, T> getCache(CacheEnum theCache) {
180                return (Cache<K, T>) myCaches.get(theCache);
181        }
182
183        public long getEstimatedSize(CacheEnum theCache) {
184                return getCache(theCache).estimatedSize();
185        }
186
187        public void invalidateCaches(CacheEnum... theCaches) {
188                for (CacheEnum next : theCaches) {
189                        getCache(next).invalidateAll();
190                }
191        }
192
193        public enum CacheEnum {
194                TAG_DEFINITION(TagDefinitionCacheKey.class),
195                RESOURCE_LOOKUP(String.class),
196                FORCED_ID_TO_PID(String.class),
197                FHIRPATH_EXPRESSION(String.class),
198                /**
199                 * Key type: {@literal Long}
200                 * Value type: {@literal Optional<String>}
201                 */
202                PID_TO_FORCED_ID(Long.class),
203                /**
204                 * TODO: JA this is duplicate with the CachingValidationSupport cache.
205                 * A better solution would be to drop this cache for this item, and to
206                 * create a new CachingValidationSupport implementation which uses
207                 * the MemoryCacheService for all of its caches.
208                 */
209                CONCEPT_TRANSLATION(TranslationQuery.class),
210                MATCH_URL(String.class),
211                CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class),
212                RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class),
213                HISTORY_COUNT(HistoryCountKey.class),
214                NAME_TO_PARTITION(String.class),
215                ID_TO_PARTITION(Integer.class);
216
217                public Class<?> getKeyType() {
218                        return myKeyType;
219                }
220
221                private final Class<?> myKeyType;
222
223                CacheEnum(Class<?> theKeyType) {
224                        myKeyType = theKeyType;
225                }
226        }
227
228        public static class TagDefinitionCacheKey {
229
230                private final TagTypeEnum myType;
231                private final String mySystem;
232                private final String myCode;
233                private final String myVersion;
234                private Boolean myUserSelected;
235                private final int myHashCode;
236
237                public TagDefinitionCacheKey(
238                                TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) {
239                        myType = theType;
240                        mySystem = theSystem;
241                        myCode = theCode;
242                        myVersion = theVersion;
243                        myUserSelected = theUserSelected;
244                        myHashCode = new HashCodeBuilder(17, 37)
245                                        .append(myType)
246                                        .append(mySystem)
247                                        .append(myCode)
248                                        .append(myVersion)
249                                        .append(myUserSelected)
250                                        .toHashCode();
251                }
252
253                @Override
254                public boolean equals(Object theO) {
255                        boolean retVal = false;
256                        if (theO instanceof TagDefinitionCacheKey) {
257                                TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO;
258
259                                retVal = new EqualsBuilder()
260                                                .append(myType, that.myType)
261                                                .append(mySystem, that.mySystem)
262                                                .append(myCode, that.myCode)
263                                                .isEquals();
264                        }
265                        return retVal;
266                }
267
268                @Override
269                public int hashCode() {
270                        return myHashCode;
271                }
272        }
273
274        public static class HistoryCountKey {
275                private final String myTypeName;
276                private final Long myInstanceId;
277                private final int myHashCode;
278
279                private HistoryCountKey(String theTypeName, Long theInstanceId) {
280                        myTypeName = theTypeName;
281                        myInstanceId = theInstanceId;
282                        myHashCode = new HashCodeBuilder()
283                                        .append(myTypeName)
284                                        .append(myInstanceId)
285                                        .toHashCode();
286                }
287
288                public static HistoryCountKey forSystem() {
289                        return new HistoryCountKey(null, null);
290                }
291
292                public static HistoryCountKey forType(@Nonnull String theType) {
293                        assert isNotBlank(theType);
294                        return new HistoryCountKey(theType, null);
295                }
296
297                public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) {
298                        assert theInstanceId != null;
299                        return new HistoryCountKey(null, theInstanceId);
300                }
301
302                @Override
303                public boolean equals(Object theO) {
304                        boolean retVal = false;
305                        if (theO instanceof HistoryCountKey) {
306                                HistoryCountKey that = (HistoryCountKey) theO;
307                                retVal = new EqualsBuilder()
308                                                .append(myTypeName, that.myTypeName)
309                                                .append(myInstanceId, that.myInstanceId)
310                                                .isEquals();
311                        }
312                        return retVal;
313                }
314
315                @Override
316                public int hashCode() {
317                        return myHashCode;
318                }
319        }
320}