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