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                        switch (next) {
075                                case NAME_TO_PARTITION:
076                                case ID_TO_PARTITION:
077                                case PID_TO_FORCED_ID:
078                                case MATCH_URL:
079                                case RESOURCE_LOOKUP_BY_FORCED_ID:
080                                case HISTORY_COUNT:
081                                case TAG_DEFINITION:
082                                case RESOURCE_CONDITIONAL_CREATE_VERSION:
083                                case FHIRPATH_EXPRESSION:
084                                default:
085                                        timeoutSeconds = SECONDS.convert(1, MINUTES);
086                                        maximumSize = 10000;
087                                        if (myStorageSettings.isMassIngestionMode()) {
088                                                timeoutSeconds = SECONDS.convert(50, MINUTES);
089                                                maximumSize = 100000;
090                                        }
091                                        break;
092                        }
093
094                        Cache<Object, Object> nextCache = CacheFactory.build(SECONDS.toMillis(timeoutSeconds), maximumSize);
095
096                        myCaches.put(next, nextCache);
097                }
098        }
099
100        public <K, T> T get(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
101                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
102                return doGet(theCache, theKey, theSupplier);
103        }
104
105        protected <K, T> T doGet(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
106                Cache<K, T> cache = getCache(theCache);
107                return cache.get(theKey, theSupplier);
108        }
109
110        /**
111         * Fetch an item from the cache if it exists, and use the loading function to
112         * obtain it otherwise.
113         * <p>
114         * This method will put the value into the cache using {@link #putAfterCommit(CacheEnum, Object, Object)}.
115         */
116        public <K, T> T getThenPutAfterCommit(CacheEnum theCache, K theKey, Function<K, T> theSupplier) {
117                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
118                T retVal = getIfPresent(theCache, theKey);
119                if (retVal == null) {
120                        retVal = theSupplier.apply(theKey);
121                        putAfterCommit(theCache, theKey, retVal);
122                }
123                return retVal;
124        }
125
126        public <K, V> V getIfPresent(CacheEnum theCache, K theKey) {
127                assert theCache.getKeyType().isAssignableFrom(theKey.getClass());
128                return doGetIfPresent(theCache, theKey);
129        }
130
131        protected <K, V> V doGetIfPresent(CacheEnum theCache, K theKey) {
132                return (V) getCache(theCache).getIfPresent(theKey);
133        }
134
135        public <K, V> void put(CacheEnum theCache, K theKey, V theValue) {
136                assert theCache.getKeyType().isAssignableFrom(theKey.getClass())
137                                : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache "
138                                                + theCache;
139                doPut(theCache, theKey, theValue);
140        }
141
142        protected <K, V> void doPut(CacheEnum theCache, K theKey, V theValue) {
143                getCache(theCache).put(theKey, theValue);
144        }
145
146        /**
147         * This method registers a transaction synchronization that puts an entry in the cache
148         * if and when the current database transaction successfully commits. If the
149         * transaction is rolled back, the key+value passed into this method will
150         * not be added to the cache.
151         * <p>
152         * This is useful for situations where you want to store something that has been
153         * resolved in the DB during the current transaction, but it's not yet guaranteed
154         * that this item will successfully save to the DB. Use this method in that case
155         * in order to avoid cache poisoning.
156         */
157        public <K, V> void putAfterCommit(CacheEnum theCache, K theKey, V theValue) {
158                assert theCache.getKeyType().isAssignableFrom(theKey.getClass())
159                                : "Key type " + theKey.getClass() + " doesn't match expected " + theCache.getKeyType() + " for cache "
160                                                + theCache;
161                if (TransactionSynchronizationManager.isSynchronizationActive()) {
162                        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
163                                @Override
164                                public void afterCommit() {
165                                        put(theCache, theKey, theValue);
166                                }
167                        });
168                } else {
169                        put(theCache, theKey, theValue);
170                }
171        }
172
173        @SuppressWarnings("unchecked")
174        public <K, V> Map<K, V> getAllPresent(CacheEnum theCache, Collection<K> theKeys) {
175                return doGetAllPresent(theCache, theKeys);
176        }
177
178        @SuppressWarnings("unchecked")
179        protected <K, V> Map<K, V> doGetAllPresent(CacheEnum theCache, Collection<K> theKeys) {
180                return (Map<K, V>) getCache(theCache).getAllPresent(theKeys);
181        }
182
183        public void invalidateAllCaches() {
184                myCaches.values().forEach(Cache::invalidateAll);
185        }
186
187        private <K, T> Cache<K, T> getCache(CacheEnum theCache) {
188                return (Cache<K, T>) myCaches.get(theCache);
189        }
190
191        public long getEstimatedSize(CacheEnum theCache) {
192                return getCache(theCache).estimatedSize();
193        }
194
195        public void invalidateCaches(CacheEnum... theCaches) {
196                for (CacheEnum next : theCaches) {
197                        getCache(next).invalidateAll();
198                }
199        }
200
201        public enum CacheEnum {
202                TAG_DEFINITION(TagDefinitionCacheKey.class),
203                /**
204                 * Key type: {@link ForcedIdCacheKey}
205                 * Value type: {@literal JpaResourceLookup}
206                 */
207                RESOURCE_LOOKUP_BY_FORCED_ID(ForcedIdCacheKey.class),
208                FHIRPATH_EXPRESSION(String.class),
209                /**
210                 * Key type: {@literal Long}
211                 * Value type: {@literal Optional<String>}
212                 */
213                PID_TO_FORCED_ID(JpaPid.class),
214                MATCH_URL(String.class),
215                RESOURCE_CONDITIONAL_CREATE_VERSION(Long.class),
216                HISTORY_COUNT(HistoryCountKey.class),
217                NAME_TO_PARTITION(String.class),
218                ID_TO_PARTITION(Integer.class);
219
220                private final Class<?> myKeyType;
221
222                CacheEnum(Class<?> theKeyType) {
223                        myKeyType = theKeyType;
224                }
225
226                public Class<?> getKeyType() {
227                        return myKeyType;
228                }
229        }
230
231        public static class TagDefinitionCacheKey {
232
233                private final TagTypeEnum myType;
234                private final String mySystem;
235                private final String myCode;
236                private final String myVersion;
237                private final int myHashCode;
238                private Boolean myUserSelected;
239
240                public TagDefinitionCacheKey(
241                                TagTypeEnum theType, String theSystem, String theCode, String theVersion, Boolean theUserSelected) {
242                        myType = theType;
243                        mySystem = theSystem;
244                        myCode = theCode;
245                        myVersion = theVersion;
246                        myUserSelected = theUserSelected;
247                        myHashCode = new HashCodeBuilder(17, 37)
248                                        .append(myType)
249                                        .append(mySystem)
250                                        .append(myCode)
251                                        .append(myVersion)
252                                        .append(myUserSelected)
253                                        .toHashCode();
254                }
255
256                @Override
257                public boolean equals(Object theO) {
258                        boolean retVal = false;
259                        if (theO instanceof TagDefinitionCacheKey) {
260                                TagDefinitionCacheKey that = (TagDefinitionCacheKey) theO;
261
262                                retVal = new EqualsBuilder()
263                                                .append(myType, that.myType)
264                                                .append(mySystem, that.mySystem)
265                                                .append(myCode, that.myCode)
266                                                .isEquals();
267                        }
268                        return retVal;
269                }
270
271                @Override
272                public int hashCode() {
273                        return myHashCode;
274                }
275        }
276
277        public static class HistoryCountKey {
278                private final String myTypeName;
279                private final Long myInstanceId;
280                private final Integer myPartitionId;
281                private final int myHashCode;
282
283                private HistoryCountKey(@Nullable String theTypeName, @Nullable JpaPid theInstanceId) {
284                        myTypeName = theTypeName;
285                        if (theInstanceId != null) {
286                                myInstanceId = theInstanceId.getId();
287                                myPartitionId = theInstanceId.getPartitionId();
288                        } else {
289                                myInstanceId = null;
290                                myPartitionId = null;
291                        }
292                        myHashCode = new HashCodeBuilder()
293                                        .append(myTypeName)
294                                        .append(myInstanceId)
295                                        .append(myPartitionId)
296                                        .toHashCode();
297                }
298
299                @Override
300                public boolean equals(Object theO) {
301                        boolean retVal = false;
302                        if (theO instanceof HistoryCountKey) {
303                                HistoryCountKey that = (HistoryCountKey) theO;
304                                retVal = new EqualsBuilder()
305                                                .append(myTypeName, that.myTypeName)
306                                                .append(myInstanceId, that.myInstanceId)
307                                                .isEquals();
308                        }
309                        return retVal;
310                }
311
312                @Override
313                public int hashCode() {
314                        return myHashCode;
315                }
316
317                public static HistoryCountKey forSystem() {
318                        return new HistoryCountKey(null, null);
319                }
320
321                public static HistoryCountKey forType(@Nonnull String theType) {
322                        assert isNotBlank(theType);
323                        return new HistoryCountKey(theType, null);
324                }
325
326                public static HistoryCountKey forInstance(@Nonnull JpaPid theInstanceId) {
327                        return new HistoryCountKey(null, theInstanceId);
328                }
329        }
330
331        public static class ForcedIdCacheKey {
332
333                private final String myResourceType;
334                private final String myResourceId;
335                private final List<Integer> myRequestPartitionIds;
336                private final int myHashCode;
337
338                public ForcedIdCacheKey(
339                                @Nullable String theResourceType,
340                                @Nonnull String theResourceId,
341                                @Nonnull RequestPartitionId theRequestPartitionId) {
342                        myResourceType = theResourceType;
343                        myResourceId = theResourceId;
344                        if (theRequestPartitionId.hasPartitionIds()) {
345                                myRequestPartitionIds = theRequestPartitionId.getPartitionIds();
346                        } else {
347                                myRequestPartitionIds = null;
348                        }
349                        myHashCode = Objects.hash(myResourceType, myResourceId, myRequestPartitionIds);
350                }
351
352                @Override
353                public String toString() {
354                        return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
355                                        .append("resType", myResourceType)
356                                        .append("resId", myResourceId)
357                                        .append("partId", myRequestPartitionIds)
358                                        .toString();
359                }
360
361                @Override
362                public boolean equals(Object theO) {
363                        if (this == theO) {
364                                return true;
365                        }
366                        if (!(theO instanceof ForcedIdCacheKey)) {
367                                return false;
368                        }
369                        ForcedIdCacheKey that = (ForcedIdCacheKey) theO;
370                        return Objects.equals(myResourceType, that.myResourceType)
371                                        && Objects.equals(myResourceId, that.myResourceId)
372                                        && Objects.equals(myRequestPartitionIds, that.myRequestPartitionIds);
373                }
374
375                @Override
376                public int hashCode() {
377                        return myHashCode;
378                }
379
380                /**
381                 * Creates and returns a new unqualified versionless IIdType instance
382                 */
383                public IIdType toIdType(FhirContext theFhirCtx) {
384                        return theFhirCtx.getVersion().newIdType(myResourceType, myResourceId);
385                }
386        }
387}