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}