001package ca.uhn.fhir.jpa.subscription.match.registry; 002 003/*- 004 * #%L 005 * HAPI FHIR Subscription Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 024import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 025import ca.uhn.fhir.jpa.cache.IResourceChangeEvent; 026import ca.uhn.fhir.jpa.cache.IResourceChangeListener; 027import ca.uhn.fhir.jpa.cache.IResourceChangeListenerCache; 028import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; 029import ca.uhn.fhir.jpa.model.sched.ISchedulerService; 030import ca.uhn.fhir.jpa.partition.SystemRequestDetails; 031import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 032import ca.uhn.fhir.jpa.searchparam.retry.Retrier; 033import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionActivatingSubscriber; 034import ca.uhn.fhir.rest.api.server.IBundleProvider; 035import ca.uhn.fhir.rest.param.TokenOrListParam; 036import ca.uhn.fhir.rest.param.TokenParam; 037import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 038import com.google.common.annotations.VisibleForTesting; 039import org.apache.commons.lang3.time.DateUtils; 040import org.hl7.fhir.instance.model.api.IBaseResource; 041import org.hl7.fhir.instance.model.api.IIdType; 042import org.hl7.fhir.r4.model.Subscription; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045import org.springframework.beans.factory.annotation.Autowired; 046 047import javax.annotation.Nonnull; 048import javax.annotation.PostConstruct; 049import javax.annotation.PreDestroy; 050import java.util.Collection; 051import java.util.HashSet; 052import java.util.List; 053import java.util.Set; 054import java.util.concurrent.Semaphore; 055import java.util.stream.Collectors; 056 057 058public class SubscriptionLoader implements IResourceChangeListener { 059 private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionLoader.class); 060 private static final int MAX_RETRIES = 60; // 60 * 5 seconds = 5 minutes 061 private static final long REFRESH_INTERVAL = DateUtils.MILLIS_PER_MINUTE; 062 063 private final Object mySyncSubscriptionsLock = new Object(); 064 @Autowired 065 private SubscriptionRegistry mySubscriptionRegistry; 066 @Autowired 067 DaoRegistry myDaoRegistry; 068 private Semaphore mySyncSubscriptionsSemaphore = new Semaphore(1); 069 @Autowired 070 private ISchedulerService mySchedulerService; 071 @Autowired 072 private SubscriptionActivatingSubscriber mySubscriptionActivatingInterceptor; 073 @Autowired 074 private ISearchParamRegistry mySearchParamRegistry; 075 @Autowired 076 private IResourceChangeListenerRegistry myResourceChangeListenerRegistry; 077 078 private SearchParameterMap mySearchParameterMap; 079 private SystemRequestDetails mySystemRequestDetails; 080 081 /** 082 * Constructor 083 */ 084 public SubscriptionLoader() { 085 super(); 086 } 087 088 @PostConstruct 089 public void registerListener() { 090 mySearchParameterMap = getSearchParameterMap(); 091 mySystemRequestDetails = SystemRequestDetails.forAllPartition(); 092 093 IResourceChangeListenerCache subscriptionCache = myResourceChangeListenerRegistry.registerResourceResourceChangeListener("Subscription", mySearchParameterMap, this, REFRESH_INTERVAL); 094 subscriptionCache.forceRefresh(); 095 } 096 097 @PreDestroy 098 public void unregisterListener() { 099 myResourceChangeListenerRegistry.unregisterResourceResourceChangeListener(this); 100 } 101 102 private boolean subscriptionsDaoExists() { 103 return myDaoRegistry != null && myDaoRegistry.isResourceTypeSupported("Subscription"); 104 } 105 106 /** 107 * Read the existing subscriptions from the database 108 */ 109 public void syncSubscriptions() { 110 if (!subscriptionsDaoExists()) { 111 return; 112 } 113 if (!mySyncSubscriptionsSemaphore.tryAcquire()) { 114 return; 115 } 116 try { 117 doSyncSubscriptionsWithRetry(); 118 } finally { 119 mySyncSubscriptionsSemaphore.release(); 120 } 121 } 122 123 @VisibleForTesting 124 public void acquireSemaphoreForUnitTest() throws InterruptedException { 125 mySyncSubscriptionsSemaphore.acquire(); 126 } 127 128 @VisibleForTesting 129 public int doSyncSubscriptionsForUnitTest() { 130 // Two passes for delete flag to take effect 131 int first = doSyncSubscriptionsWithRetry(); 132 int second = doSyncSubscriptionsWithRetry(); 133 return first + second; 134 } 135 136 synchronized int doSyncSubscriptionsWithRetry() { 137 // retry runs MAX_RETRIES times 138 // and if errors result every time, it will fail 139 Retrier<Integer> syncSubscriptionRetrier = new Retrier<>(this::doSyncSubscriptions, MAX_RETRIES); 140 return syncSubscriptionRetrier.runWithRetry(); 141 } 142 143 private int doSyncSubscriptions() { 144 if (mySchedulerService.isStopping()) { 145 return 0; 146 } 147 148 synchronized (mySyncSubscriptionsLock) { 149 ourLog.debug("Starting sync subscriptions"); 150 151 IBundleProvider subscriptionBundleList = getSubscriptionDao().search(mySearchParameterMap, mySystemRequestDetails); 152 153 Integer subscriptionCount = subscriptionBundleList.size(); 154 assert subscriptionCount != null; 155 if (subscriptionCount >= SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS) { 156 ourLog.error("Currently over " + SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS + " subscriptions. Some subscriptions have not been loaded."); 157 } 158 159 List<IBaseResource> resourceList = subscriptionBundleList.getResources(0, subscriptionCount); 160 161 return updateSubscriptionRegistry(resourceList); 162 } 163 } 164 165 private IFhirResourceDao<?> getSubscriptionDao() { 166 return myDaoRegistry.getSubscriptionDao(); 167 } 168 169 @Nonnull 170 private SearchParameterMap getSearchParameterMap() { 171 SearchParameterMap map = new SearchParameterMap(); 172 173 if (mySearchParamRegistry.getActiveSearchParam("Subscription", "status") != null) { 174 map.add(Subscription.SP_STATUS, new TokenOrListParam() 175 .addOr(new TokenParam(null, Subscription.SubscriptionStatus.REQUESTED.toCode())) 176 .addOr(new TokenParam(null, Subscription.SubscriptionStatus.ACTIVE.toCode()))); 177 } 178 map.setLoadSynchronousUpTo(SubscriptionConstants.MAX_SUBSCRIPTION_RESULTS); 179 return map; 180 } 181 182 private int updateSubscriptionRegistry(List<IBaseResource> theResourceList) { 183 Set<String> allIds = new HashSet<>(); 184 int activatedCount = 0; 185 int registeredCount = 0; 186 187 for (IBaseResource resource : theResourceList) { 188 String nextId = resource.getIdElement().getIdPart(); 189 allIds.add(nextId); 190 191 // internally, subscriptions that cannot activate 192 // will be set to error 193 boolean activated = mySubscriptionActivatingInterceptor.activateSubscriptionIfRequired(resource); 194 if (activated) { 195 activatedCount++; 196 } 197 else { 198 logSubscriptionNotActivatedPlusErrorIfPossible(resource); 199 } 200 201 boolean registered = mySubscriptionRegistry.registerSubscriptionUnlessAlreadyRegistered(resource); 202 if (registered) { 203 registeredCount++; 204 } 205 } 206 207 mySubscriptionRegistry.unregisterAllSubscriptionsNotInCollection(allIds); 208 ourLog.debug("Finished sync subscriptions - activated {} and registered {}", theResourceList.size(), registeredCount); 209 return activatedCount; 210 } 211 212 /** 213 * Logs 214 * @param theSubscription 215 */ 216 private void logSubscriptionNotActivatedPlusErrorIfPossible(IBaseResource theSubscription) { 217 String error; 218 if (theSubscription instanceof Subscription) { 219 error = ((Subscription) theSubscription).getError(); 220 } 221 else if (theSubscription instanceof org.hl7.fhir.dstu3.model.Subscription) { 222 error = ((org.hl7.fhir.dstu3.model.Subscription) theSubscription).getError(); 223 } 224 else if (theSubscription instanceof org.hl7.fhir.dstu2.model.Subscription) { 225 error = ((org.hl7.fhir.dstu2.model.Subscription) theSubscription).getError(); 226 } 227 else { 228 error = ""; 229 } 230 ourLog.error("Subscription " 231 + theSubscription.getIdElement().getIdPart() 232 + " could not be activated." 233 + " This will not prevent startup, but it could lead to undesirable outcomes! " 234 + error 235 ); 236 } 237 238 @Override 239 public void handleInit(Collection<IIdType> theResourceIds) { 240 if (!subscriptionsDaoExists()) { 241 ourLog.warn("Subsriptions are enabled on this server, but there is no Subscription DAO configured."); 242 return; 243 } 244 IFhirResourceDao<?> subscriptionDao = getSubscriptionDao(); 245 SystemRequestDetails systemRequestDetails = SystemRequestDetails.forAllPartition(); 246 List<IBaseResource> resourceList = theResourceIds.stream().map(n -> subscriptionDao.read(n, systemRequestDetails)).collect(Collectors.toList()); 247 updateSubscriptionRegistry(resourceList); 248 } 249 250 @Override 251 public void handleChange(IResourceChangeEvent theResourceChangeEvent) { 252 // For now ignore the contents of theResourceChangeEvent. In the future, consider updating the registry based on 253 // known subscriptions that have been created, updated & deleted 254 syncSubscriptions(); 255 } 256} 257