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