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