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