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}