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