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}