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}