001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.impl.cluster; 018 019import java.time.Duration; 020import java.util.HashSet; 021import java.util.Optional; 022import java.util.Set; 023import java.util.concurrent.ScheduledExecutorService; 024import java.util.concurrent.TimeUnit; 025import java.util.concurrent.atomic.AtomicBoolean; 026import java.util.stream.Collectors; 027 028import org.apache.camel.CamelContext; 029import org.apache.camel.CamelContextAware; 030import org.apache.camel.Route; 031import org.apache.camel.ServiceStatus; 032import org.apache.camel.StartupListener; 033import org.apache.camel.api.management.ManagedAttribute; 034import org.apache.camel.api.management.ManagedResource; 035import org.apache.camel.cluster.CamelClusterEventListener; 036import org.apache.camel.cluster.CamelClusterMember; 037import org.apache.camel.cluster.CamelClusterService; 038import org.apache.camel.cluster.CamelClusterView; 039import org.apache.camel.spi.CamelEvent; 040import org.apache.camel.spi.CamelEvent.CamelContextStartedEvent; 041import org.apache.camel.support.DefaultConsumer; 042import org.apache.camel.support.EventNotifierSupport; 043import org.apache.camel.support.RoutePolicySupport; 044import org.apache.camel.support.cluster.ClusterServiceHelper; 045import org.apache.camel.support.cluster.ClusterServiceSelectors; 046import org.apache.camel.util.ObjectHelper; 047import org.apache.camel.util.ReferenceCount; 048import org.slf4j.Logger; 049import org.slf4j.LoggerFactory; 050 051@ManagedResource(description = "Clustered Route policy using") 052public final class ClusteredRoutePolicy extends RoutePolicySupport implements CamelContextAware { 053 054 private static final Logger LOG = LoggerFactory.getLogger(ClusteredRoutePolicy.class); 055 056 private final AtomicBoolean leader; 057 private final Set<Route> startedRoutes; 058 private final Set<Route> stoppedRoutes; 059 private final ReferenceCount refCount; 060 private final CamelClusterEventListener.Leadership leadershipEventListener; 061 private final CamelContextStartupListener listener; 062 private final AtomicBoolean contextStarted; 063 064 private final String namespace; 065 private final CamelClusterService.Selector clusterServiceSelector; 066 private CamelClusterService clusterService; 067 private CamelClusterView clusterView; 068 069 private Duration initialDelay; 070 private ScheduledExecutorService executorService; 071 072 private CamelContext camelContext; 073 074 private ClusteredRoutePolicy(CamelClusterService clusterService, CamelClusterService.Selector clusterServiceSelector, String namespace) { 075 this.namespace = namespace; 076 this.clusterService = clusterService; 077 this.clusterServiceSelector = clusterServiceSelector; 078 079 ObjectHelper.notNull(namespace, "Namespace"); 080 081 this.leadershipEventListener = new CamelClusterLeadershipListener(); 082 083 this.stoppedRoutes = new HashSet<>(); 084 this.startedRoutes = new HashSet<>(); 085 this.leader = new AtomicBoolean(false); 086 this.contextStarted = new AtomicBoolean(false); 087 this.initialDelay = Duration.ofMillis(0); 088 089 try { 090 this.listener = new CamelContextStartupListener(); 091 this.listener.start(); 092 } catch (Exception e) { 093 throw new RuntimeException(e); 094 } 095 096 // Cleanup the policy when all the routes it manages have been shut down 097 // so a single policy instance can be shared among routes. 098 this.refCount = ReferenceCount.onRelease(() -> { 099 if (camelContext != null) { 100 camelContext.getManagementStrategy().removeEventNotifier(listener); 101 if (executorService != null) { 102 camelContext.getExecutorServiceManager().shutdownNow(executorService); 103 } 104 } 105 106 try { 107 // Remove event listener 108 clusterView.removeEventListener(leadershipEventListener); 109 110 // If all the routes have been shut down then the view and its 111 // resources can eventually be released. 112 clusterView.getClusterService().releaseView(clusterView); 113 } catch (Exception e) { 114 throw new RuntimeException(e); 115 } finally { 116 setLeader(false); 117 } 118 }); 119 } 120 121 @Override 122 public CamelContext getCamelContext() { 123 return camelContext; 124 } 125 126 @Override 127 public void setCamelContext(CamelContext camelContext) { 128 if (this.camelContext == camelContext) { 129 return; 130 } 131 132 if (this.camelContext != null && this.camelContext != camelContext) { 133 throw new IllegalStateException("CamelContext should not be changed: current=" + this.camelContext + ", new=" + camelContext); 134 } 135 136 try { 137 this.camelContext = camelContext; 138 this.camelContext.addStartupListener(this.listener); 139 this.camelContext.getManagementStrategy().addEventNotifier(this.listener); 140 this.executorService = camelContext.getExecutorServiceManager().newSingleThreadScheduledExecutor(this, "ClusteredRoutePolicy"); 141 } catch (Exception e) { 142 throw new RuntimeException(e); 143 } 144 } 145 146 public Duration getInitialDelay() { 147 return initialDelay; 148 } 149 150 public void setInitialDelay(Duration initialDelay) { 151 this.initialDelay = initialDelay; 152 } 153 154 // **************************************************** 155 // life-cycle 156 // **************************************************** 157 158 private ServiceStatus getStatus(Route route) { 159 if (camelContext != null) { 160 ServiceStatus answer = camelContext.getRouteController().getRouteStatus(route.getId()); 161 if (answer == null) { 162 answer = ServiceStatus.Stopped; 163 } 164 return answer; 165 } 166 return null; 167 } 168 169 @Override 170 public void onInit(Route route) { 171 super.onInit(route); 172 173 LOG.info("Route managed by {}. Setting route {} AutoStartup flag to false.", getClass(), route.getId()); 174 route.getRouteContext().setAutoStartup(false); 175 176 this.refCount.retain(); 177 this.stoppedRoutes.add(route); 178 179 startManagedRoutes(); 180 } 181 182 @Override 183 public void doStart() throws Exception { 184 if (clusterService == null) { 185 clusterService = ClusterServiceHelper.lookupService(camelContext, clusterServiceSelector) 186 .orElseThrow(() -> new IllegalStateException("CamelCluster service not found")); 187 } 188 189 LOG.debug("ClusteredRoutePolicy {} is using ClusterService instance {} (id={}, type={})", this, clusterService, clusterService.getId(), 190 clusterService.getClass().getName()); 191 192 clusterView = clusterService.getView(namespace); 193 } 194 195 @Override 196 public void doShutdown() throws Exception { 197 this.refCount.release(); 198 } 199 200 // **************************************************** 201 // Management 202 // **************************************************** 203 204 @ManagedAttribute(description = "Is this route the master or a slave") 205 public boolean isLeader() { 206 return leader.get(); 207 } 208 209 // **************************************************** 210 // Route managements 211 // **************************************************** 212 213 private synchronized void setLeader(boolean isLeader) { 214 if (isLeader && leader.compareAndSet(false, isLeader)) { 215 LOG.debug("Leadership taken"); 216 startManagedRoutes(); 217 } else if (!isLeader && leader.getAndSet(isLeader)) { 218 LOG.debug("Leadership lost"); 219 stopManagedRoutes(); 220 } 221 } 222 223 private void startManagedRoutes() { 224 if (isLeader()) { 225 doStartManagedRoutes(); 226 } else { 227 // If the leadership has been lost in the meanwhile, stop any 228 // eventually started route 229 doStopManagedRoutes(); 230 } 231 } 232 233 private void doStartManagedRoutes() { 234 if (!isRunAllowed()) { 235 return; 236 } 237 238 try { 239 for (Route route : stoppedRoutes) { 240 ServiceStatus status = getStatus(route); 241 if (status != null && status.isStartable()) { 242 LOG.debug("Starting route '{}'", route.getId()); 243 camelContext.getRouteController().startRoute(route.getId()); 244 245 startedRoutes.add(route); 246 } 247 } 248 249 stoppedRoutes.removeAll(startedRoutes); 250 } catch (Exception e) { 251 handleException(e); 252 } 253 } 254 255 private void stopManagedRoutes() { 256 if (isLeader()) { 257 // If became a leader in the meanwhile, start any eventually stopped 258 // route 259 doStartManagedRoutes(); 260 } else { 261 doStopManagedRoutes(); 262 } 263 } 264 265 private void doStopManagedRoutes() { 266 if (!isRunAllowed()) { 267 return; 268 } 269 270 try { 271 for (Route route : startedRoutes) { 272 ServiceStatus status = getStatus(route); 273 if (status != null && status.isStoppable()) { 274 LOG.debug("Stopping route '{}'", route.getId()); 275 stopRoute(route); 276 277 stoppedRoutes.add(route); 278 } 279 } 280 281 startedRoutes.removeAll(stoppedRoutes); 282 } catch (Exception e) { 283 handleException(e); 284 } 285 } 286 287 private void onCamelContextStarted() { 288 LOG.debug("Apply cluster policy (stopped-routes='{}', started-routes='{}')", stoppedRoutes.stream().map(Route::getId).collect(Collectors.joining(",")), 289 startedRoutes.stream().map(Route::getId).collect(Collectors.joining(","))); 290 291 clusterView.addEventListener(leadershipEventListener); 292 } 293 294 // **************************************************** 295 // Event handling 296 // **************************************************** 297 298 private class CamelClusterLeadershipListener implements CamelClusterEventListener.Leadership { 299 @Override 300 public void leadershipChanged(CamelClusterView view, Optional<CamelClusterMember> leader) { 301 setLeader(clusterView.getLocalMember().isLeader()); 302 } 303 } 304 305 private class CamelContextStartupListener extends EventNotifierSupport implements StartupListener { 306 @Override 307 public void notify(CamelEvent event) throws Exception { 308 onCamelContextStarted(); 309 } 310 311 @Override 312 public boolean isEnabled(CamelEvent event) { 313 return event instanceof CamelContextStartedEvent; 314 } 315 316 @Override 317 public void onCamelContextStarted(CamelContext context, boolean alreadyStarted) throws Exception { 318 if (alreadyStarted) { 319 // Invoke it only if the context was already started as this 320 // method is not invoked at last event as documented but after 321 // routes warm-up so this is useful for routes deployed after 322 // the camel context has been started-up. For standard routes 323 // configuration the notification of the camel context started 324 // is provided by EventNotifier. 325 // 326 // We should check why this callback is not invoked at latest 327 // stage, or maybe rename it as it is misleading and provide a 328 // better alternative for intercept camel events. 329 onCamelContextStarted(); 330 } 331 } 332 333 private void onCamelContextStarted() { 334 // Start managing the routes only when the camel context is started 335 // so start/stop of managed routes do not clash with CamelContext 336 // startup 337 if (contextStarted.compareAndSet(false, true)) { 338 339 // Eventually delay the startup of the routes a later time 340 if (initialDelay.toMillis() > 0) { 341 LOG.debug("Policy will be effective in {}", initialDelay); 342 executorService.schedule(ClusteredRoutePolicy.this::onCamelContextStarted, initialDelay.toMillis(), TimeUnit.MILLISECONDS); 343 } else { 344 ClusteredRoutePolicy.this.onCamelContextStarted(); 345 } 346 } 347 } 348 } 349 350 // **************************************************** 351 // Static helpers 352 // **************************************************** 353 354 public static ClusteredRoutePolicy forNamespace(CamelContext camelContext, CamelClusterService.Selector selector, String namespace) throws Exception { 355 ClusteredRoutePolicy policy = new ClusteredRoutePolicy(null, selector, namespace); 356 policy.setCamelContext(camelContext); 357 358 return policy; 359 } 360 361 public static ClusteredRoutePolicy forNamespace(CamelContext camelContext, String namespace) throws Exception { 362 return forNamespace(camelContext, ClusterServiceSelectors.DEFAULT_SELECTOR, namespace); 363 } 364 365 public static ClusteredRoutePolicy forNamespace(CamelClusterService service, String namespace) throws Exception { 366 return new ClusteredRoutePolicy(service, ClusterServiceSelectors.DEFAULT_SELECTOR, namespace); 367 } 368 369 public static ClusteredRoutePolicy forNamespace(CamelClusterService.Selector selector, String namespace) throws Exception { 370 return new ClusteredRoutePolicy(null, selector, namespace); 371 } 372 373 public static ClusteredRoutePolicy forNamespace(String namespace) throws Exception { 374 return forNamespace(ClusterServiceSelectors.DEFAULT_SELECTOR, namespace); 375 } 376}