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