001/*-
002 * #%L
003 * HAPI FHIR Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2023 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        private static final int MAX_RETRIES = 60;
047
048        private static Instant ourNowForUnitTests;
049
050        @Autowired
051        IResourceChangeListenerCacheRefresher myResourceChangeListenerCacheRefresher;
052        @Autowired
053        SearchParamMatcher mySearchParamMatcher;
054
055        private final String myResourceName;
056        private final IResourceChangeListener myResourceChangeListener;
057        private final SearchParameterMap mySearchParameterMap;
058        private final ResourceVersionCache myResourceVersionCache = new ResourceVersionCache();
059        private final long myRemoteRefreshIntervalMs;
060
061        private boolean myInitialized = false;
062        private Instant myNextRefreshTime = Instant.MIN;
063
064        public ResourceChangeListenerCache(String theResourceName, IResourceChangeListener theResourceChangeListener, SearchParameterMap theSearchParameterMap, long theRemoteRefreshIntervalMs) {
065                myResourceName = theResourceName;
066                myResourceChangeListener = theResourceChangeListener;
067                mySearchParameterMap = SerializationUtils.clone(theSearchParameterMap);
068                myRemoteRefreshIntervalMs = theRemoteRefreshIntervalMs;
069        }
070
071        /**
072         * Request that the cache be refreshed at the next convenient time (in a different thread)
073         */
074        @Override
075        public void requestRefresh() {
076                myNextRefreshTime = Instant.MIN;
077        }
078
079        /**
080         * Request that a cache be refreshed now, in the current thread
081         */
082        @Override
083        public ResourceChangeResult forceRefresh() {
084                requestRefresh();
085                return refreshCacheWithRetry();
086        }
087
088        /**
089         * Refresh the cache if theResource matches our SearchParameterMap
090         * @param theResource
091         */
092        public void requestRefreshIfWatching(IBaseResource theResource) {
093                if (matches(theResource)) {
094                        requestRefresh();
095                }
096        }
097
098        public boolean matches(IBaseResource theResource) {
099                InMemoryMatchResult result = mySearchParamMatcher.match(mySearchParameterMap, theResource);
100                if (!result.supported()) {
101                        // This should never happen since we enforce only in-memory SearchParamMaps at registration time
102                        throw new IllegalStateException(Msg.code(483) + "Search Parameter Map " + mySearchParameterMap + " cannot be processed in-memory: " + result.getUnsupportedReason());
103                }
104                return result.matched();
105        }
106
107        @Override
108        public ResourceChangeResult refreshCacheIfNecessary() {
109                ResourceChangeResult retval = new ResourceChangeResult();
110                if (isTimeToRefresh()) {
111                        retval = refreshCacheWithRetry();
112                }
113                return retval;
114        }
115
116        protected boolean isTimeToRefresh() {
117                return myNextRefreshTime.isBefore(now());
118        }
119
120        private static Instant now() {
121                if (ourNowForUnitTests != null) {
122                        return ourNowForUnitTests;
123                }
124                return Instant.now();
125        }
126
127        public ResourceChangeResult refreshCacheWithRetry() {
128                ResourceChangeResult retval;
129                try {
130                        retval = refreshCacheAndNotifyListenersWithRetry();
131                } finally {
132                        myNextRefreshTime = now().plus(Duration.ofMillis(myRemoteRefreshIntervalMs));
133                }
134                return retval;
135        }
136
137        @VisibleForTesting
138        public void setResourceChangeListenerCacheRefresher(IResourceChangeListenerCacheRefresher theResourceChangeListenerCacheRefresher) {
139                myResourceChangeListenerCacheRefresher = theResourceChangeListenerCacheRefresher;
140        }
141
142        private ResourceChangeResult refreshCacheAndNotifyListenersWithRetry() {
143                Retrier<ResourceChangeResult> refreshCacheRetrier = new Retrier<>(() -> {
144                        synchronized (this) {
145                                return myResourceChangeListenerCacheRefresher.refreshCacheAndNotifyListener(this);
146                        }
147                }, MAX_RETRIES);
148                return refreshCacheRetrier.runWithRetry();
149        }
150
151        @Override
152        public Instant getNextRefreshTime() {
153                return myNextRefreshTime;
154        }
155
156        @Override
157        public SearchParameterMap getSearchParameterMap() {
158                return mySearchParameterMap;
159        }
160
161        @Override
162        public boolean isInitialized() {
163                return myInitialized;
164        }
165
166        public ResourceChangeListenerCache setInitialized(boolean theInitialized) {
167                myInitialized = theInitialized;
168                return this;
169        }
170
171        @Override
172        public String getResourceName() {
173                return myResourceName;
174        }
175
176        public ResourceVersionCache getResourceVersionCache() {
177                return myResourceVersionCache;
178        }
179
180        public IResourceChangeListener getResourceChangeListener() {
181                return myResourceChangeListener;
182        }
183
184        /**
185         * @param theTime has format like "12:34:56" i.e. HH:MM:SS
186         */
187        @VisibleForTesting
188        public static void setNowForUnitTests(String theTime) {
189                if (theTime == null) {
190                        ourNowForUnitTests = null;
191                        return;
192                }
193                String datetime = "2020-11-16T" + theTime + "Z";
194                Clock clock = Clock.fixed(Instant.parse(datetime), ZoneId.systemDefault());
195                ourNowForUnitTests = Instant.now(clock);
196        }
197
198        @VisibleForTesting
199        Instant getNextRefreshTimeForUnitTest() {
200                return myNextRefreshTime;
201        }
202
203        @VisibleForTesting
204        public void clearForUnitTest() {
205                requestRefresh();
206                myResourceVersionCache.clear();
207        }
208
209        @Override
210        public String toString() {
211                return new ToStringBuilder(this)
212                        .append("myResourceName", myResourceName)
213                        .append("mySearchParameterMap", mySearchParameterMap)
214                        .append("myInitialized", myInitialized)
215                        .toString();
216        }
217}