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                                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                CONCEPT_TRANSLATION(TranslationQuery.class),
204                MATCH_URL(String.class),
205                CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class),
206                RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class),
207                HISTORY_COUNT(HistoryCountKey.class),
208                NAME_TO_PARTITION(String.class),
209                ID_TO_PARTITION(Integer.class);
210
211                public Class<?> getKeyType() {
212                        return myKeyType;
213                }
214
215                private final Class<?> myKeyType;
216
217                CacheEnum(Class<?> theKeyType) {
218                        myKeyType = theKeyType;
219                }
220        }
221
222        public static class TagDefinitionCacheKey {
223
224                private final TagTypeEnum myType;
225                private final String mySystem;
226                private final String myCode;
227                private final String myVersion;
228                private Boolean myUserSelected;
229                private final int myHashCode;
230
231                public TagDefinitionCacheKey(
232                                TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) {
233                        myType = theType;
234                        mySystem = theSystem;
235                        myCode = theCode;
236                        myVersion = theVersion;
237                        myUserSelected = theUserSelected;
238                        myHashCode = new HashCodeBuilder(17, 37)
239                                        .append(myType)
240                                        .append(mySystem)
241                                        .append(myCode)
242                                        .append(myVersion)
243                                        .append(myUserSelected)
244                                        .toHashCode();
245                }
246
247                @Override
248                public boolean equals(Object theO) {
249                        boolean retVal = false;
250                        if (theO instanceof TagDefinitionCacheKey) {
251                                TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO;
252
253                                retVal = new EqualsBuilder()
254                                                .append(myType, that.myType)
255                                                .append(mySystem, that.mySystem)
256                                                .append(myCode, that.myCode)
257                                                .isEquals();
258                        }
259                        return retVal;
260                }
261
262                @Override
263                public int hashCode() {
264                        return myHashCode;
265                }
266        }
267
268        public static class HistoryCountKey {
269                private final String myTypeName;
270                private final Long myInstanceId;
271                private final int myHashCode;
272
273                private HistoryCountKey(String theTypeName, Long theInstanceId) {
274                        myTypeName = theTypeName;
275                        myInstanceId = theInstanceId;
276                        myHashCode = new HashCodeBuilder()
277                                        .append(myTypeName)
278                                        .append(myInstanceId)
279                                        .toHashCode();
280                }
281
282                public static HistoryCountKey forSystem() {
283                        return new HistoryCountKey(null, null);
284                }
285
286                public static HistoryCountKey forType(@Nonnull String theType) {
287                        assert isNotBlank(theType);
288                        return new HistoryCountKey(theType, null);
289                }
290
291                public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) {
292                        assert theInstanceId != null;
293                        return new HistoryCountKey(null, theInstanceId);
294                }
295
296                @Override
297                public boolean equals(Object theO) {
298                        boolean retVal = false;
299                        if (theO instanceof HistoryCountKey) {
300                                HistoryCountKey that = (HistoryCountKey) theO;
301                                retVal = new EqualsBuilder()
302                                                .append(myTypeName, that.myTypeName)
303                                                .append(myInstanceId, that.myInstanceId)
304                                                .isEquals();
305                        }
306                        return retVal;
307                }
308
309                @Override
310                public int hashCode() {
311                        return myHashCode;
312                }
313        }
314}