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