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.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.List;
024import java.util.Map;
025import java.util.Objects;
026import java.util.Set;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.ConcurrentMap;
029import java.util.concurrent.CopyOnWriteArraySet;
030import java.util.concurrent.TimeUnit;
031import java.util.stream.Collectors;
032
033import org.apache.camel.CamelContext;
034import org.apache.camel.NamedNode;
035import org.apache.camel.Route;
036import org.apache.camel.RuntimeCamelException;
037import org.apache.camel.cluster.CamelClusterService;
038import org.apache.camel.impl.engine.DefaultRouteController;
039import org.apache.camel.meta.Experimental;
040import org.apache.camel.model.RouteDefinition;
041import org.apache.camel.spi.RoutePolicy;
042import org.apache.camel.spi.RoutePolicyFactory;
043import org.apache.camel.support.cluster.ClusterServiceHelper;
044import org.apache.camel.support.cluster.ClusterServiceSelectors;
045import org.apache.camel.support.service.ServiceHelper;
046import org.apache.camel.util.ObjectHelper;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050@Experimental
051public class ClusteredRouteController extends DefaultRouteController {
052    private static final Logger LOGGER = LoggerFactory.getLogger(ClusteredRouteController.class);
053
054    private final Set<String> routes;
055    private final ConcurrentMap<String, ClusteredRouteConfiguration> configurations;
056    private final List<ClusteredRouteFilter> filters;
057    private final PolicyFactory policyFactory;
058    private final ClusteredRouteConfiguration defaultConfiguration;
059    private CamelClusterService clusterService;
060    private CamelClusterService.Selector clusterServiceSelector;
061
062    public ClusteredRouteController() {
063        this.routes = new CopyOnWriteArraySet<>();
064        this.configurations = new ConcurrentHashMap<>();
065        this.filters = new ArrayList<>();
066        this.clusterServiceSelector = ClusterServiceSelectors.DEFAULT_SELECTOR;
067        this.policyFactory = new PolicyFactory();
068
069        this.defaultConfiguration = new ClusteredRouteConfiguration();
070        this.defaultConfiguration.setInitialDelay(Duration.ofMillis(0));
071    }
072
073    // *******************************
074    // Properties.
075    // *******************************
076
077    /**
078     * Add a filter used to to filter cluster aware routes.
079     */
080    public void addFilter(ClusteredRouteFilter filter) {
081        this.filters.add(filter);
082    }
083
084    /**
085     * Sets the filters used to filter cluster aware routes.
086     */
087    public void setFilters(Collection<ClusteredRouteFilter> filters) {
088        this.filters.clear();
089        this.filters.addAll(filters);
090    }
091
092    public Collection<ClusteredRouteFilter> getFilters() {
093        return Collections.unmodifiableList(filters);
094    }
095
096    /**
097     * Add a configuration for the given route.
098     */
099    public void addRouteConfiguration(String routeId, ClusteredRouteConfiguration configuration) {
100        configurations.put(routeId, configuration);
101    }
102
103    /**
104     * Sets the configurations for the routes.
105     */
106    public void setRoutesConfiguration(Map<String, ClusteredRouteConfiguration> configurations) {
107        this.configurations.clear();
108        this.configurations.putAll(configurations);
109    }
110
111    public Map<String, ClusteredRouteConfiguration> getRoutesConfiguration() {
112        return Collections.unmodifiableMap(this.configurations);
113    }
114
115    public Duration getInitialDelay() {
116        return this.defaultConfiguration.getInitialDelay();
117    }
118
119    /**
120     * Set the amount of time the route controller should wait before to start
121     * the routes after the camel context is started.
122     *
123     * @param initialDelay the initial delay.
124     */
125    public void setInitialDelay(Duration initialDelay) {
126        this.defaultConfiguration.setInitialDelay(initialDelay);
127    }
128
129    public String getNamespace() {
130        return this.defaultConfiguration.getNamespace();
131    }
132
133    /**
134     * Set the default namespace.
135     */
136    public void setNamespace(String namespace) {
137        this.defaultConfiguration.setNamespace(namespace);
138    }
139
140    public CamelClusterService getClusterService() {
141        return clusterService;
142    }
143
144    /**
145     * Set the cluster service to use.
146     */
147    public void setClusterService(CamelClusterService clusterService) {
148        ObjectHelper.notNull(clusterService, "CamelClusterService");
149
150        this.clusterService = clusterService;
151    }
152
153    public CamelClusterService.Selector getClusterServiceSelector() {
154        return clusterServiceSelector;
155    }
156
157    /**
158     * Set the selector strategy to look-up a {@link CamelClusterService}
159     */
160    public void setClusterServiceSelector(CamelClusterService.Selector clusterServiceSelector) {
161        ObjectHelper.notNull(clusterService, "CamelClusterService.Selector");
162
163        this.clusterServiceSelector = clusterServiceSelector;
164    }
165
166    // *******************************
167    //
168    // *******************************
169
170    @Override
171    public Collection<Route> getControlledRoutes() {
172        return this.routes.stream().map(getCamelContext()::getRoute).filter(Objects::nonNull).collect(Collectors.toList());
173    }
174
175    @Override
176    public void doStart() throws Exception {
177        final CamelContext context = getCamelContext();
178
179        // Parameters validation
180        ObjectHelper.notNull(defaultConfiguration.getNamespace(), "Namespace");
181        ObjectHelper.notNull(defaultConfiguration.getInitialDelay(), "initialDelay");
182        ObjectHelper.notNull(context, "camelContext");
183
184        if (clusterService == null) {
185            // Finally try to grab it from the camel context.
186            clusterService = ClusterServiceHelper.mandatoryLookupService(context, clusterServiceSelector);
187        }
188
189        LOGGER.debug("Using ClusterService instance {} (id={}, type={})", clusterService, clusterService.getId(), clusterService.getClass().getName());
190
191        if (!ServiceHelper.isStarted(clusterService)) {
192            // Start the cluster service if not yet started.
193            clusterService.start();
194        }
195
196        super.doStart();
197    }
198
199    @Override
200    public void doStop() throws Exception {
201        if (ServiceHelper.isStarted(clusterService)) {
202            // Stop the cluster service.
203            clusterService.stop();
204        }
205    }
206
207    @Override
208    public void setCamelContext(CamelContext camelContext) {
209        if (!camelContext.getRoutePolicyFactories().contains(this.policyFactory)) {
210            camelContext.addRoutePolicyFactory(this.policyFactory);
211        }
212
213        super.setCamelContext(camelContext);
214    }
215
216    // *******************************
217    // Route operations are disabled
218    // *******************************
219
220    @Override
221    public void startRoute(String routeId) throws Exception {
222        failIfClustered(routeId);
223
224        // Delegate to default impl.
225        super.startRoute(routeId);
226    }
227
228    @Override
229    public void stopRoute(String routeId) throws Exception {
230        failIfClustered(routeId);
231
232        // Delegate to default impl.
233        super.stopRoute(routeId);
234    }
235
236    @Override
237    public void stopRoute(String routeId, long timeout, TimeUnit timeUnit) throws Exception {
238        failIfClustered(routeId);
239
240        // Delegate to default impl.
241        super.stopRoute(routeId, timeout, timeUnit);
242    }
243
244    @Override
245    public boolean stopRoute(String routeId, long timeout, TimeUnit timeUnit, boolean abortAfterTimeout) throws Exception {
246        failIfClustered(routeId);
247
248        // Delegate to default impl.
249        return super.stopRoute(routeId, timeout, timeUnit, abortAfterTimeout);
250    }
251
252    @Override
253    public void suspendRoute(String routeId) throws Exception {
254        failIfClustered(routeId);
255
256        // Delegate to default impl.
257        super.suspendRoute(routeId);
258    }
259
260    @Override
261    public void suspendRoute(String routeId, long timeout, TimeUnit timeUnit) throws Exception {
262        failIfClustered(routeId);
263
264        // Delegate to default impl.
265        super.suspendRoute(routeId, timeout, timeUnit);
266    }
267
268    @Override
269    public void resumeRoute(String routeId) throws Exception {
270        failIfClustered(routeId);
271
272        // Delegate to default impl.
273        super.resumeRoute(routeId);
274    }
275
276    // *******************************
277    // Helpers
278    // *******************************
279
280    private void failIfClustered(String routeId) {
281        // Can't perform action on routes managed by this controller as they
282        // are clustered and they may be part of the same view.
283        if (routes.contains(routeId)) {
284            throw new UnsupportedOperationException("Operation not supported as route " + routeId + " is clustered");
285        }
286    }
287
288    // *******************************
289    // Factories
290    // *******************************
291
292    private final class PolicyFactory implements RoutePolicyFactory {
293        @Override
294        public RoutePolicy createRoutePolicy(CamelContext camelContext, String routeId, NamedNode node) {
295            RouteDefinition route = (RouteDefinition)node;
296            // All the filter have to be match to include the route in the
297            // clustering set-up
298            if (filters.stream().allMatch(filter -> filter.test(camelContext, routeId, route))) {
299
300                if (ObjectHelper.isNotEmpty(route.getRoutePolicies())) {
301                    // Check if the route is already configured with a clustered
302                    // route policy, in that case exclude it.
303                    if (route.getRoutePolicies().stream().anyMatch(ClusteredRoutePolicy.class::isInstance)) {
304                        LOGGER.debug("Route '{}' has a ClusteredRoutePolicy already set-up", routeId);
305                        return null;
306                    }
307                }
308
309                try {
310                    final ClusteredRouteConfiguration configuration = configurations.getOrDefault(routeId, defaultConfiguration);
311                    final String namespace = ObjectHelper.supplyIfEmpty(configuration.getNamespace(), defaultConfiguration::getNamespace);
312                    final Duration initialDelay = ObjectHelper.supplyIfEmpty(configuration.getInitialDelay(), defaultConfiguration::getInitialDelay);
313
314                    ClusteredRoutePolicy policy = ClusteredRoutePolicy.forNamespace(clusterService, namespace);
315                    policy.setCamelContext(getCamelContext());
316                    policy.setInitialDelay(initialDelay);
317
318                    LOGGER.debug("Attaching route '{}' to namespace '{}'", routeId, namespace);
319
320                    routes.add(routeId);
321
322                    return policy;
323                } catch (Exception e) {
324                    throw RuntimeCamelException.wrapRuntimeCamelException(e);
325                }
326            }
327
328            return null;
329        }
330    }
331}