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.interceptor.api.HookParams; 024import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; 025import ca.uhn.fhir.interceptor.api.Pointcut; 026import ca.uhn.fhir.jpa.subscription.channel.subscription.ISubscriptionDeliveryChannelNamer; 027import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelRegistry; 028import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscription; 029import ca.uhn.fhir.jpa.subscription.model.ChannelRetryConfiguration; 030import ca.uhn.fhir.util.HapiExtensions; 031import org.apache.commons.lang3.Validate; 032import org.hl7.fhir.instance.model.api.IBaseResource; 033import org.hl7.fhir.instance.model.api.IIdType; 034import org.hl7.fhir.r4.model.Subscription; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037import org.springframework.beans.factory.annotation.Autowired; 038 039import javax.annotation.PreDestroy; 040import java.util.Collection; 041import java.util.Collections; 042import java.util.List; 043import java.util.Optional; 044 045/** 046 * Cache of active subscriptions. When a new subscription is added to the cache, a new Spring Channel is created 047 * and a new MessageHandler for that subscription is subscribed to that channel. These subscriptions, channels, and 048 * handlers are all caches in this registry so they can be removed it the subscription is deleted. 049 */ 050 051public class SubscriptionRegistry { 052 private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionRegistry.class); 053 private final ActiveSubscriptionCache myActiveSubscriptionCache = new ActiveSubscriptionCache(); 054 @Autowired 055 private SubscriptionCanonicalizer mySubscriptionCanonicalizer; 056 @Autowired 057 private ISubscriptionDeliveryChannelNamer mySubscriptionDeliveryChannelNamer; 058 @Autowired 059 private SubscriptionChannelRegistry mySubscriptionChannelRegistry; 060 @Autowired 061 private IInterceptorBroadcaster myInterceptorBroadcaster; 062 063 /** 064 * Constructor 065 */ 066 public SubscriptionRegistry() { 067 super(); 068 } 069 070 public ActiveSubscription get(String theIdPart) { 071 return myActiveSubscriptionCache.get(theIdPart); 072 } 073 074 public Collection<ActiveSubscription> getAll() { 075 return myActiveSubscriptionCache.getAll(); 076 } 077 078 private Optional<CanonicalSubscription> hasSubscription(IIdType theId) { 079 Validate.notNull(theId); 080 Validate.notBlank(theId.getIdPart()); 081 Optional<ActiveSubscription> activeSubscription = Optional.ofNullable(myActiveSubscriptionCache.get(theId.getIdPart())); 082 return activeSubscription.map(ActiveSubscription::getSubscription); 083 } 084 085 /** 086 * Extracts the retry configuration settings from the CanonicalSubscription object. 087 * 088 * Returns the configuration, or null, if no retry (or a bad retry value) 089 * is specified. 090 * 091 * @param theSubscription 092 * @return 093 */ 094 private ChannelRetryConfiguration getRetryConfigurationFromSubscriptionExtensions(CanonicalSubscription theSubscription) { 095 ChannelRetryConfiguration configuration = new ChannelRetryConfiguration(); 096 097 List<String> retryCount = theSubscription.getChannelExtensions(HapiExtensions.EX_RETRY_COUNT); 098 if (retryCount.size() == 1) { 099 String val = retryCount.get(0); 100 configuration.setRetryCount(Integer.parseInt(val)); 101 } 102 // else - 0 or more than 1 means no retry policy at all 103 104 // retry count is required for any retry policy 105 if (configuration.getRetryCount() == null || configuration.getRetryCount() < 0) { 106 configuration = null; 107 } 108 109 return configuration; 110 } 111 112 private void registerSubscription(IIdType theId, CanonicalSubscription theCanonicalSubscription) { 113 Validate.notNull(theId); 114 String subscriptionId = theId.getIdPart(); 115 Validate.notBlank(subscriptionId); 116 Validate.notNull(theCanonicalSubscription); 117 118 String channelName = mySubscriptionDeliveryChannelNamer.nameFromSubscription(theCanonicalSubscription); 119 120 // get the actual retry configuration 121 ChannelRetryConfiguration configuration = getRetryConfigurationFromSubscriptionExtensions(theCanonicalSubscription); 122 123 ActiveSubscription activeSubscription = new ActiveSubscription(theCanonicalSubscription, channelName); 124 activeSubscription.setRetryConfiguration(configuration); 125 126 // add to our registries 127 mySubscriptionChannelRegistry.add(activeSubscription); 128 myActiveSubscriptionCache.put(subscriptionId, activeSubscription); 129 130 ourLog.info("Registered active subscription Subscription/{} - Have {} registered", subscriptionId, myActiveSubscriptionCache.size()); 131 132 // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED 133 HookParams params = new HookParams() 134 .add(CanonicalSubscription.class, theCanonicalSubscription); 135 myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, params); 136 } 137 138 public void unregisterSubscriptionIfRegistered(String theSubscriptionId) { 139 Validate.notNull(theSubscriptionId); 140 141 ActiveSubscription activeSubscription = myActiveSubscriptionCache.remove(theSubscriptionId); 142 if (activeSubscription != null) { 143 mySubscriptionChannelRegistry.remove(activeSubscription); 144 ourLog.info("Unregistered active subscription {} - Have {} registered", theSubscriptionId, myActiveSubscriptionCache.size()); 145 146 // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED 147 HookParams params = new HookParams(); 148 myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_UNREGISTERED, params); 149 } 150 } 151 152 @PreDestroy 153 public void unregisterAllSubscriptions() { 154 // Once to set flag 155 unregisterAllSubscriptionsNotInCollection(Collections.emptyList()); 156 // Twice to remove 157 unregisterAllSubscriptionsNotInCollection(Collections.emptyList()); 158 } 159 160 void unregisterAllSubscriptionsNotInCollection(Collection<String> theAllIds) { 161 162 List<String> idsToDelete = myActiveSubscriptionCache.markAllSubscriptionsNotInCollectionForDeletionAndReturnIdsToDelete(theAllIds); 163 for (String id : idsToDelete) { 164 unregisterSubscriptionIfRegistered(id); 165 } 166 } 167 168 public synchronized boolean registerSubscriptionUnlessAlreadyRegistered(IBaseResource theSubscription) { 169 Validate.notNull(theSubscription); 170 Optional<CanonicalSubscription> existingSubscription = hasSubscription(theSubscription.getIdElement()); 171 CanonicalSubscription newSubscription = mySubscriptionCanonicalizer.canonicalize(theSubscription); 172 173 if (existingSubscription.isPresent()) { 174 if (newSubscription.equals(existingSubscription.get())) { 175 // No changes 176 return false; 177 } 178 ourLog.info("Updating already-registered active subscription {}", theSubscription.getIdElement().toUnqualified().getValue()); 179 if (channelTypeSame(existingSubscription.get(), newSubscription)) { 180 ourLog.info("Channel type is same. Updating active subscription and re-using existing channel and handlers."); 181 updateSubscription(theSubscription); 182 return true; 183 } 184 unregisterSubscriptionIfRegistered(theSubscription.getIdElement().getIdPart()); 185 } 186 if (Subscription.SubscriptionStatus.ACTIVE.equals(newSubscription.getStatus())) { 187 registerSubscription(theSubscription.getIdElement(), newSubscription); 188 return true; 189 } else { 190 return false; 191 } 192 } 193 194 private void updateSubscription(IBaseResource theSubscription) { 195 IIdType theId = theSubscription.getIdElement(); 196 Validate.notNull(theId); 197 Validate.notBlank(theId.getIdPart()); 198 ActiveSubscription activeSubscription = myActiveSubscriptionCache.get(theId.getIdPart()); 199 Validate.notNull(activeSubscription); 200 CanonicalSubscription canonicalized = mySubscriptionCanonicalizer.canonicalize(theSubscription); 201 activeSubscription.setSubscription(canonicalized); 202 203 // Interceptor call: SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED 204 HookParams params = new HookParams() 205 .add(CanonicalSubscription.class, canonicalized); 206 myInterceptorBroadcaster.callHooks(Pointcut.SUBSCRIPTION_AFTER_ACTIVE_SUBSCRIPTION_REGISTERED, params); 207 } 208 209 private boolean channelTypeSame(CanonicalSubscription theExistingSubscription, CanonicalSubscription theNewSubscription) { 210 return theExistingSubscription.getChannelType().equals(theNewSubscription.getChannelType()); 211 } 212 213 public int size() { 214 return myActiveSubscriptionCache.size(); 215 } 216}