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.context.FhirContext;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
025import ca.uhn.fhir.jpa.api.model.TranslationQuery;
026import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
027import ca.uhn.fhir.sl.cache.Cache;
028import ca.uhn.fhir.sl.cache.CacheFactory;
029import jakarta.annotation.Nonnull;
030import jakarta.annotation.Nullable;
031import org.apache.commons.lang3.builder.EqualsBuilder;
032import org.apache.commons.lang3.builder.HashCodeBuilder;
033import org.hl7.fhir.instance.model.api.IIdType;
034import org.springframework.transaction.support.TransactionSynchronization;
035import org.springframework.transaction.support.TransactionSynchronizationManager;
036
037import java.util.Collection;
038import java.util.EnumMap;
039import java.util.Map;
040import java.util.Objects;
041import java.util.function.Function;
042
043import static java.util.concurrent.TimeUnit.MINUTES;
044import static java.util.concurrent.TimeUnit.SECONDS;
045import static org.apache.commons.lang3.StringUtils.isNotBlank;
046
047/**
048 * This class acts as a central spot for all of the many Caffeine caches we use in HAPI FHIR.
049 * <p>
050 * The API is super simplistic, and caches are all 1-minute, max 10000 entries for starters. We could definitely add nuance to this,
051 * which will be much easier now that this is being centralized. Some logging/monitoring would be good too.
052 */
053// TODO: JA2 extract an interface for this class and use it everywhere
054public class MemoryCacheService {
055
056        private final JpaStorageSettings myStorageSettings;
057        private final EnumMap<CacheEnum, Cache<?, ?>> myCaches = new EnumMap<>(CacheEnum.class);
058
059        public MemoryCacheService(JpaStorageSettings theStorageSettings) {
060                myStorageSettings = theStorageSettings;
061
062                populateCaches();
063        }
064
065        private void populateCaches() {
066                for (CacheEnum next : CacheEnum.values()) {
067
068                        long timeoutSeconds;
069                        int maximumSize;
070
071                        switch (next) {
072                                case CONCEPT_TRANSLATION:
073                                case CONCEPT_TRANSLATION_REVERSE:
074                                        timeoutSeconds =
075                                                        SECONDS.convert(myStorageSettings.getTranslationCachesExpireAfterWriteInMinutes(), MINUTES);
076                                        maximumSize = 500000;
077                                        break;
078                                case PID_TO_FORCED_ID:
079                                case FORCED_ID_TO_PID:
080                                case MATCH_URL:
081                                case RESOURCE_LOOKUP_BY_FORCED_ID:
082                                case HISTORY_COUNT:
083                                case TAG_DEFINITION:
084                                case RESOURCE_CONDITIONAL_CREATE_VERSION:
085                                case FHIRPATH_EXPRESSION:
086                                default:
087                                        timeoutSeconds = SECONDS.convert(1, MINUTES);
088                                        maximumSize = 10000;
089                                        if (myStorageSettings.isMassIngestionMode()) {
090                                                timeoutSeconds = SECONDS.convert(50, MINUTES);
091                                                maximumSize = 100000;
092                                        }
093                                        break;
094                        }
095
096                        Cache<Object, Object> nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize);
097
098                        myCaches.put(next, nextCache);
099                }
100        }
101
102        public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
103                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
104                return doGet(theCache, theKey, theSupplier);
105        }
106
107        protected <K, T> T doGet(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
108                Cache<K, T> cache = getCache(theCache);
109                return cache.get(theKey, theSupplier);
110        }
111
112        /**
113         * Fetch an item from the cache if it exists, and use the loading function to
114         * obtain it otherwise.
115         * <p>
116         * This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}.
117         */
118        public <K, T> T getThenPutAfterCommit(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
119                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
120                T retVal = getIfPresent(theCache, theKey);
121                if (retVal == null) {
122                        retVal = theSupplier.apply(theKey);
123                        putAfterCommit(theCache, theKey, retVal);
124                }
125                return retVal;
126        }
127
128        public <K, V> V getIfPresent(CacheEnum theCache, K theKey) {
129                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
130                return doGetIfPresent(theCache, theKey);
131        }
132
133        protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) {
134                return (V) getCache(theCache).getIfPresent(theKey);
135        }
136
137        public <K, V> void put(CacheEnum theCache, K theKey, V theValue) {
138                assert theCache.getKeyType().isAssignableFrom(theKey.getClass())
139                                : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache "
140                                                + theCache;
141                doPut(theCache, theKey, theValue);
142        }
143
144        protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) {
145                getCache(theCache).put(theKey, theValue);
146        }
147
148        /**
149         * This method registers a transaction synchronization that puts an entry in the cache
150         * if and when the current database transaction successfully commits. If the
151         * transaction is rolled back, the key+value passed into this method will
152         * not be added to the cache.
153         * <p>
154         * This is useful for situations where you want to store something that has been
155         * resolved in the DB during the current transaction, but it's not yet guaranteed
156         * that this item will successfully save to the DB. Use this method in that case
157         * in order to avoid cache poisoning.
158         */
159        public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) {
160                assert theCache.getKeyType().isAssignableFrom(theKey.getClass())
161                                : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache "
162                                                + theCache;
163                if (TransactionSynchronizationManager.isSynchronizationActive()) {
164                        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
165                                @Override
166                                public void afterCommit() {
167                                        put(theCache, theKey, theValue);
168                                }
169                        });
170                } else {
171                        put(theCache, theKey, theValue);
172                }
173        }
174
175        @SuppressWarnings("unchecked")
176        public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) {
177                return doGetAllPresent(theCache, theKeys);
178        }
179
180        @SuppressWarnings("unchecked")
181        protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) {
182                return (Map<K, V>) getCache(theCache).getAllPresent(theKeys);
183        }
184
185        public void invalidateAllCaches() {
186                myCaches.values().forEach(Cache::invalidateAll);
187        }
188
189        private <K, T> Cache<K, T> getCache(CacheEnum theCache) {
190                return (Cache<K, T>) myCaches.get(theCache);
191        }
192
193        public long getEstimatedSize(CacheEnum theCache) {
194                return getCache(theCache).estimatedSize();
195        }
196
197        public void invalidateCaches(CacheEnum... theCaches) {
198                for (CacheEnum next : theCaches) {
199                        getCache(next).invalidateAll();
200                }
201        }
202
203        public enum CacheEnum {
204                TAG_DEFINITION(TagDefinitionCacheKey.class),
205                /**
206                 * Key type: {@link ForcedIdCacheKey}
207                 * Value type: {@literal JpaResourceLookup}
208                 */
209                RESOURCE_LOOKUP_BY_FORCED_ID(ForcedIdCacheKey.class),
210                FORCED_ID_TO_PID(String.class),
211                FHIRPATH_EXPRESSION(String.class),
212                /**
213                 * Key type: {@literal Long}
214                 * Value type: {@literal Optional<String>}
215                 */
216                PID_TO_FORCED_ID(Long.class),
217                /**
218                 * TODO: JA this is duplicate with the CachingValidationSupport cache.
219                 * A better solution would be to drop this cache for this item, and to
220                 * create a new CachingValidationSupport implementation which uses
221                 * the MemoryCacheService for all of its caches.
222                 */
223                CONCEPT_TRANSLATION(TranslationQuery.class),
224                MATCH_URL(String.class),
225                CONCEPT_TRANSLATION_REVERSE(TranslationQuery.class),
226                RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class),
227                HISTORY_COUNT(HistoryCountKey.class),
228                NAME_TO_PARTITION(String.class),
229                ID_TO_PARTITION(Integer.class);
230
231                public Class<?> getKeyType() {
232                        return myKeyType;
233                }
234
235                private final Class<?> myKeyType;
236
237                CacheEnum(Class<?> theKeyType) {
238                        myKeyType = theKeyType;
239                }
240        }
241
242        public static class TagDefinitionCacheKey {
243
244                private final TagTypeEnum myType;
245                private final String mySystem;
246                private final String myCode;
247                private final String myVersion;
248                private Boolean myUserSelected;
249                private final int myHashCode;
250
251                public TagDefinitionCacheKey(
252                                TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) {
253                        myType = theType;
254                        mySystem = theSystem;
255                        myCode = theCode;
256                        myVersion = theVersion;
257                        myUserSelected = theUserSelected;
258                        myHashCode = new HashCodeBuilder(17, 37)
259                                        .append(myType)
260                                        .append(mySystem)
261                                        .append(myCode)
262                                        .append(myVersion)
263                                        .append(myUserSelected)
264                                        .toHashCode();
265                }
266
267                @Override
268                public boolean equals(Object theO) {
269                        boolean retVal = false;
270                        if (theO instanceof TagDefinitionCacheKey) {
271                                TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO;
272
273                                retVal = new EqualsBuilder()
274                                                .append(myType, that.myType)
275                                                .append(mySystem, that.mySystem)
276                                                .append(myCode, that.myCode)
277                                                .isEquals();
278                        }
279                        return retVal;
280                }
281
282                @Override
283                public int hashCode() {
284                        return myHashCode;
285                }
286        }
287
288        public static class HistoryCountKey {
289                private final String myTypeName;
290                private final Long myInstanceId;
291                private final int myHashCode;
292
293                private HistoryCountKey(String theTypeName, Long theInstanceId) {
294                        myTypeName = theTypeName;
295                        myInstanceId = theInstanceId;
296                        myHashCode = new HashCodeBuilder()
297                                        .append(myTypeName)
298                                        .append(myInstanceId)
299                                        .toHashCode();
300                }
301
302                public static HistoryCountKey forSystem() {
303                        return new HistoryCountKey(null, null);
304                }
305
306                public static HistoryCountKey forType(@Nonnull String theType) {
307                        assert isNotBlank(theType);
308                        return new HistoryCountKey(theType, null);
309                }
310
311                public static HistoryCountKey forInstance(@Nonnull Long theInstanceId) {
312                        assert theInstanceId != null;
313                        return new HistoryCountKey(null, theInstanceId);
314                }
315
316                @Override
317                public boolean equals(Object theO) {
318                        boolean retVal = false;
319                        if (theO instanceof HistoryCountKey) {
320                                HistoryCountKey that = (HistoryCountKey) theO;
321                                retVal = new EqualsBuilder()
322                                                .append(myTypeName, that.myTypeName)
323                                                .append(myInstanceId, that.myInstanceId)
324                                                .isEquals();
325                        }
326                        return retVal;
327                }
328
329                @Override
330                public int hashCode() {
331                        return myHashCode;
332                }
333        }
334
335        public static class ForcedIdCacheKey {
336
337                private final String myResourceType;
338                private final String myResourceId;
339                private final RequestPartitionId myRequestPartitionId;
340                private final int myHashCode;
341
342                public ForcedIdCacheKey(
343                                @Nullable String theResourceType,
344                                @Nonnull String theResourceId,
345                                @Nonnull RequestPartitionId theRequestPartitionId) {
346                        myResourceType = theResourceType;
347                        myResourceId = theResourceId;
348                        myRequestPartitionId = theRequestPartitionId;
349                        myHashCode = Objects.hash(myResourceType, myResourceId, myRequestPartitionId);
350                }
351
352                @Override
353                public boolean equals(Object theO) {
354                        if (this == theO) {
355                                return true;
356                        }
357                        if (!(theO instanceof ForcedIdCacheKey)) {
358                                return false;
359                        }
360                        ForcedIdCacheKey that = (ForcedIdCacheKey) theO;
361                        return Objects.equals(myResourceType, that.myResourceType)
362                                        && Objects.equals(myResourceId, that.myResourceId)
363                                        && Objects.equals(myRequestPartitionId, that.myRequestPartitionId);
364                }
365
366                @Override
367                public int hashCode() {
368                        return myHashCode;
369                }
370
371                /**
372                 * Creates and returns a new unqualified versionless IIdType instance
373                 */
374                public IIdType toIdType(FhirContext theFhirCtx) {
375                        if (myResourceType == null) {
376                                return toIdTypeWithoutResourceType(theFhirCtx);
377                        }
378                        IIdType retVal = theFhirCtx.getVersion().newIdType();
379                        retVal.setValue(myResourceType + "/" + myResourceId);
380                        return retVal;
381                }
382
383                /**
384                 * Creates and returns a new unqualified versionless IIdType instance
385                 */
386                public IIdType toIdTypeWithoutResourceType(FhirContext theFhirCtx) {
387                        IIdType retVal = theFhirCtx.getVersion().newIdType();
388                        retVal.setValue(myResourceId);
389                        return retVal;
390                }
391        }
392}