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}