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