
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}