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