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