001/*-
002 * #%L
003 * HAPI FHIR JPA - Search Parameters
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.cache;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
024import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
025import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher;
026import ca.uhn.fhir.jpa.searchparam.retry.Retrier;
027import com.google.common.annotations.VisibleForTesting;
028import org.apache.commons.lang3.SerializationUtils;
029import org.apache.commons.lang3.builder.ToStringBuilder;
030import org.hl7.fhir.instance.model.api.IBaseResource;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.springframework.beans.factory.annotation.Autowired;
034import org.springframework.context.annotation.Scope;
035import org.springframework.stereotype.Component;
036
037import java.time.Clock;
038import java.time.Duration;
039import java.time.Instant;
040import java.time.ZoneId;
041
042@Component
043@Scope("prototype")
044public class ResourceChangeListenerCache implements IResourceChangeListenerCache {
045        private static final Logger ourLog = LoggerFactory.getLogger(ResourceChangeListenerCache.class);
046        /**
047         * Max number of retries to do for cache refreshing
048         */
049        private static final int MAX_RETRIES = 60;
050
051        private static Instant ourNowForUnitTests;
052
053        @Autowired
054        IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher;
055
056        @Autowired
057        SearchParamMatcher mySearchParamMatcher;
058
059        private final String myResourceName;
060        private final IResourceChangeListener myResourceChangeListener;
061        private final SearchParameterMap mySearchParameterMap;
062        private final ResourceVersionCache myResourceVersionCache = new ResourceVersionCache();
063        private final long myRemoteRefreshIntervalMs;
064
065        private boolean myInitialized = false;
066        private Instant myNextRefreshTime = Instant.MIN;
067
068        public ResourceChangeListenerCache(
069                        String theResourceName,
070                        IResourceChangeListener theResourceChangeListener,
071                        SearchParameterMap theSearchParameterMap,
072                        long theRemoteRefreshIntervalMs) {
073                myResourceName = theResourceName;
074                myResourceChangeListener = theResourceChangeListener;
075                mySearchParameterMap = SerializationUtils.clone(theSearchParameterMap);
076                myRemoteRefreshIntervalMs = theRemoteRefreshIntervalMs;
077        }
078
079        /**
080         * Request that the cache be refreshed at the next convenient time (in a different thread)
081         */
082        @Override
083        public void requestRefresh() {
084                myNextRefreshTime = Instant.MIN;
085        }
086
087        /**
088         * Request that a cache be refreshed now, in the current thread
089         */
090        @Override
091        public ResourceChangeResult forceRefresh() {
092                requestRefresh();
093                return refreshCacheWithRetry();
094        }
095
096        /**
097         * Refresh the cache if theResource matches our SearchParameterMap
098         * @param theResource
099         */
100        public void requestRefreshIfWatching(IBaseResource theResource) {
101                if (matches(theResource)) {
102                        requestRefresh();
103                }
104        }
105
106        public boolean matches(IBaseResource theResource) {
107                InMemoryMatchResult result = mySearchParamMatcher.match(mySearchParameterMap, theResource);
108                if (!result.supported()) {
109                        // This should never happen since we enforce only in-memory SearchParamMaps at registration time
110                        throw new IllegalStateException(Msg.code(483) + "Search Parameter Map " + mySearchParameterMap
111                                        + " cannot be processed in-memory: " + result.getUnsupportedReason());
112                }
113                return result.matched();
114        }
115
116        @Override
117        public ResourceChangeResult refreshCacheIfNecessary() {
118                ResourceChangeResult retval = new ResourceChangeResult();
119                if (isTimeToRefresh()) {
120                        retval = refreshCacheWithRetry();
121                }
122                return retval;
123        }
124
125        protected boolean isTimeToRefresh() {
126                return myNextRefreshTime.isBefore(now());
127        }
128
129        static Instant now() {
130                if (ourNowForUnitTests != null) {
131                        return ourNowForUnitTests;
132                }
133                return Instant.now();
134        }
135
136        public ResourceChangeResult refreshCacheWithRetry() {
137                ResourceChangeResult retval;
138                try {
139                        retval = refreshCacheAndNotifyListenersWithRetry();
140                } finally {
141                        myNextRefreshTime = now().plus(Duration.ofMillis(myRemoteRefreshIntervalMs));
142                }
143                return retval;
144        }
145
146        @VisibleForTesting
147        public void setResourceChangeListenerCacheRefresher(
148                        IResourceChangeListenerCacheRefresher theResourceChangeListenerCacheRefresher) {
149                myResourceChangeListenerCacheRefresher = theResourceChangeListenerCacheRefresher;
150        }
151
152        private ResourceChangeResult refreshCacheAndNotifyListenersWithRetry() {
153                Retrier<ResourceChangeResult> refreshCacheRetrier = new Retrier<>(
154                                () -> {
155                                        synchronized (this) {
156                                                return myResourceChangeListenerCacheRefresher.refreshCacheAndNotifyListener(this);
157                                        }
158                                },
159                                getMaxRetries());
160                return refreshCacheRetrier.runWithRetry();
161        }
162
163        @Override
164        public Instant getNextRefreshTime() {
165                return myNextRefreshTime;
166        }
167
168        @Override
169        public SearchParameterMap getSearchParameterMap() {
170                return mySearchParameterMap;
171        }
172
173        @Override
174        public boolean isInitialized() {
175                return myInitialized;
176        }
177
178        public ResourceChangeListenerCache setInitialized(boolean theInitialized) {
179                myInitialized = theInitialized;
180                return this;
181        }
182
183        @Override
184        public String getResourceName() {
185                return myResourceName;
186        }
187
188        public ResourceVersionCache getResourceVersionCache() {
189                return myResourceVersionCache;
190        }
191
192        public IResourceChangeListener getResourceChangeListener() {
193                return myResourceChangeListener;
194        }
195
196        /**
197         * @param theTime has format like "12:34:56" i.e. HH:MM:SS
198         */
199        @VisibleForTesting
200        public static void setNowForUnitTests(String theTime) {
201                if (theTime == null) {
202                        ourNowForUnitTests = null;
203                        return;
204                }
205                String datetime = "2020-11-16T" + theTime + "Z";
206                Clock clock = Clock.fixed(Instant.parse(datetime), ZoneId.systemDefault());
207                ourNowForUnitTests = Instant.now(clock);
208        }
209
210        @VisibleForTesting
211        Instant getNextRefreshTimeForUnitTest() {
212                return myNextRefreshTime;
213        }
214
215        @VisibleForTesting
216        public void clearForUnitTest() {
217                requestRefresh();
218                myResourceVersionCache.clear();
219        }
220
221        @Override
222        public String toString() {
223                return new ToStringBuilder(this)
224                                .append("myResourceName", myResourceName)
225                                .append("mySearchParameterMap", mySearchParameterMap)
226                                .append("myInitialized", myInitialized)
227                                .toString();
228        }
229
230        static int getMaxRetries() {
231                return MAX_RETRIES;
232        }
233}