org.apache.camel.impl.engine.InternalRouteStartupManager Maven / Gradle / Ivy
Show all versions of camel-base-engine Show documentation
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.impl.engine;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.camel.Consumer;
import org.apache.camel.Endpoint;
import org.apache.camel.FailedToStartRouteException;
import org.apache.camel.LoggingLevel;
import org.apache.camel.MultipleConsumersSupport;
import org.apache.camel.Route;
import org.apache.camel.ServiceStatus;
import org.apache.camel.StartupListener;
import org.apache.camel.StartupStep;
import org.apache.camel.StatefulService;
import org.apache.camel.SuspendableService;
import org.apache.camel.spi.CamelLogger;
import org.apache.camel.spi.LifecycleStrategy;
import org.apache.camel.spi.RouteStartupOrder;
import org.apache.camel.support.OrderedComparator;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Internal route startup manager used by {@link AbstractCamelContext} to safely start internal route services during
* starting routes.
*
* This code has been refactored out of {@link AbstractCamelContext} to its own class.
*/
final class InternalRouteStartupManager {
private static final Logger LOG = LoggerFactory.getLogger(InternalRouteStartupManager.class);
private final Lock lock = new ReentrantLock();
private final ThreadLocal setupRoute = new ThreadLocal<>();
private final CamelLogger routeLogger = new CamelLogger(LOG);
private int defaultRouteStartupOrder = 1000;
/**
* If Camel is currently starting up a route then this returns the route.
*/
public Route getSetupRoute() {
return setupRoute.get();
}
/**
* Initializes the routes
*
* @param routeServices the routes to initialize
* @throws Exception is thrown if error initializing routes
*/
public void doInitRoutes(AbstractCamelContext camelContext, Map routeServices)
throws Exception {
camelContext.setStartingRoutes(true);
try {
for (RouteService routeService : routeServices.values()) {
StartupStep step = camelContext.getCamelContextExtension().getStartupStepRecorder().beginStep(Route.class,
routeService.getId(),
"Init Route");
try {
LOG.debug("Initializing route id: {}", routeService.getId());
setupRoute.set(routeService.getRoute());
// initializing route is called doSetup as we do not want to change the service state on the RouteService
// so it can remain as stopped, when Camel is booting as this was the previous behavior - otherwise its state
// would be initialized
routeService.setUp();
} finally {
setupRoute.remove();
camelContext.getCamelContextExtension().getStartupStepRecorder().endStep(step);
}
}
} finally {
camelContext.setStartingRoutes(false);
}
}
/**
* Starts or resumes the routes
*
* @param routeServices the routes to start (will only start a route if its not already started)
* @param checkClash whether to check for startup ordering clash
* @param startConsumer whether the route consumer should be started. Can be used to warmup the route without
* starting the consumer.
* @param resumeConsumer whether the route consumer should be resumed.
* @param addingRoutes whether we are adding new routes
* @throws Exception is thrown if error starting routes
*/
public void doStartOrResumeRoutes(
AbstractCamelContext camelContext,
Map routeServices, boolean checkClash, boolean startConsumer, boolean resumeConsumer,
boolean addingRoutes)
throws Exception {
camelContext.setStartingRoutes(true);
try {
// filter out already started routes
Map filtered = new LinkedHashMap<>();
for (Map.Entry entry : routeServices.entrySet()) {
final boolean startable = isStartable(entry);
if (startable) {
filtered.put(entry.getKey(), entry.getValue());
}
}
// the context is in last phase of staring, so lets start the routes
safelyStartRouteServices(camelContext, checkClash, startConsumer, resumeConsumer, addingRoutes, filtered.values());
} finally {
camelContext.setStartingRoutes(false);
}
}
private static boolean isStartable(Map.Entry entry) {
boolean startable = false;
Consumer consumer = entry.getValue().getRoute().getConsumer();
if (consumer instanceof SuspendableService suspendableService) {
// consumer could be suspended, which is not reflected in
// the BaseRouteService status
startable = suspendableService.isSuspended();
}
if (!startable && consumer instanceof StatefulService statefulService) {
// consumer could be stopped, which is not reflected in the
// BaseRouteService status
startable = statefulService.getStatus().isStartable();
} else if (!startable) {
// no consumer so use state from route service
startable = entry.getValue().getStatus().isStartable();
}
return startable;
}
/**
* Starts the routes services in a proper manner which ensures the routes will be started in correct order, check
* for clash and that the routes will also be shutdown in correct order as well.
*
* This method must be used to start routes in a safe manner.
*
* @param checkClash whether to check for startup order clash
* @param startConsumer whether the route consumer should be started. Can be used to warmup the route without
* starting the consumer.
* @param resumeConsumer whether the route consumer should be resumed.
* @param addingRoutes whether we are adding new routes
* @param routeServices the routes
* @throws Exception is thrown if error starting the routes
*/
private void safelyStartRouteServices(
AbstractCamelContext camelContext,
boolean checkClash, boolean startConsumer, boolean resumeConsumer, boolean addingRoutes,
Collection routeServices)
throws Exception {
lock.lock();
try {
// list of inputs to start when all the routes have been prepared for
// starting
// we use a tree map so the routes will be ordered according to startup
// order defined on the route
Map inputs = new TreeMap<>();
// figure out the order in which the routes should be started
for (RouteService routeService : routeServices) {
DefaultRouteStartupOrder order = doPrepareRouteToBeStarted(camelContext, routeService);
// check for clash before we add it as input
if (checkClash) {
doCheckStartupOrderClash(camelContext, order, inputs);
}
inputs.put(order.getStartupOrder(), order);
}
// warm up routes before we start them
doWarmUpRoutes(camelContext, inputs, startConsumer);
// sort the startup listeners so they are started in the right order
camelContext.getStartupListeners().sort(OrderedComparator.get());
// now call the startup listeners where the routes has been warmed up
// (only the actual route consumer has not yet been started)
for (StartupListener startup : camelContext.getStartupListeners()) {
startup.onCamelContextStarted(camelContext.getCamelContextReference(), camelContext.isStarted());
}
// because the consumers may also register startup listeners we need to
// reset
// the already started listeners
List backup = new ArrayList<>(camelContext.getStartupListeners());
camelContext.getStartupListeners().clear();
// now start the consumers
if (startConsumer) {
if (resumeConsumer) {
// and now resume the routes
doResumeRouteConsumers(camelContext, inputs, addingRoutes);
} else {
// and now start the routes
// and check for clash with multiple consumers of the same
// endpoints which is not allowed
doStartRouteConsumers(camelContext, inputs, addingRoutes);
}
}
// sort the startup listeners so they are started in the right order
camelContext.getStartupListeners().sort(OrderedComparator.get());
// now the consumers that was just started may also add new
// StartupListeners (such as timer)
// so we need to ensure they get started as well
for (StartupListener startup : camelContext.getStartupListeners()) {
startup.onCamelContextStarted(camelContext.getCamelContextReference(), camelContext.isStarted());
}
// and add the previous started startup listeners to the list so we have
// them all
camelContext.getStartupListeners().addAll(0, backup);
// inputs no longer needed
inputs.clear();
} finally {
lock.unlock();
}
}
/**
* @see #safelyStartRouteServices(AbstractCamelContext, boolean, boolean, boolean, boolean, Collection)
*/
public void safelyStartRouteServices(
AbstractCamelContext camelContext,
boolean forceAutoStart, boolean checkClash, boolean startConsumer, boolean resumeConsumer, boolean addingRoutes,
RouteService... routeServices)
throws Exception {
lock.lock();
try {
safelyStartRouteServices(camelContext, checkClash, startConsumer, resumeConsumer, addingRoutes,
Arrays.asList(routeServices));
} finally {
lock.unlock();
}
}
DefaultRouteStartupOrder doPrepareRouteToBeStarted(AbstractCamelContext camelContext, RouteService routeService) {
// add the inputs from this route service to the list to start
// afterwards
// should be ordered according to the startup number
Integer startupOrder = routeService.getRoute().getStartupOrder();
if (startupOrder == null) {
// auto assign a default startup order
startupOrder = defaultRouteStartupOrder++;
}
// create holder object that contains information about this route to be
// started
Route route = routeService.getRoute();
return new DefaultRouteStartupOrder(startupOrder, route, routeService);
}
boolean doCheckStartupOrderClash(
AbstractCamelContext camelContext, DefaultRouteStartupOrder answer, Map inputs)
throws FailedToStartRouteException {
// check for clash by startupOrder id
DefaultRouteStartupOrder other = inputs.get(answer.getStartupOrder());
if (other != null && answer != other) {
String otherId = other.getRoute().getId();
throw new FailedToStartRouteException(
answer.getRoute().getId(), "startupOrder clash. Route " + otherId + " already has startupOrder " + answer
.getStartupOrder() + " configured which this route have as well. Please correct startupOrder to be unique among all your routes.");
}
// check in existing already started as well
for (RouteStartupOrder order : camelContext.getCamelContextExtension().getRouteStartupOrder()) {
String otherId = order.getRoute().getId();
// skip clash check if it's the same route id, as it's the same
// route (can happen when using suspend/resume)
if (!answer.getRoute().getId().equals(otherId)
&& answer.getStartupOrder() == order.getStartupOrder()) {
throw new FailedToStartRouteException(
answer.getRoute().getId(), "startupOrder clash. Route " + otherId + " already has startupOrder "
+ answer.getStartupOrder()
+ " configured which this route have as well. Please correct startupOrder to be unique among all your routes.");
}
}
return true;
}
void doWarmUpRoutes(AbstractCamelContext camelContext, Map inputs, boolean autoStartup)
throws FailedToStartRouteException {
// now prepare the routes by starting its services before we start the
// input
for (Map.Entry entry : inputs.entrySet()) {
// defer starting inputs till later as we want to prepare the routes
// by starting
// all their processors and child services etc.
// then later we open the floods to Camel by starting the inputs
// what this does is to ensure Camel is more robust on starting
// routes as all routes
// will then be prepared in time before we start inputs which will
// consume messages to be routed
RouteService routeService = entry.getValue().getRouteService();
StartupStep step = camelContext.getCamelContextExtension().getStartupStepRecorder().beginStep(Route.class,
routeService.getId(),
"Warump Route");
try {
LOG.debug("Warming up route id: {} having autoStartup={}", routeService.getId(), autoStartup);
setupRoute.set(routeService.getRoute());
// ensure we setup before warmup
routeService.setUp();
routeService.warmUp();
} finally {
setupRoute.remove();
camelContext.getCamelContextExtension().getStartupStepRecorder().endStep(step);
}
}
}
void doResumeRouteConsumers(
AbstractCamelContext camelContext, Map inputs, boolean addingRoutes)
throws Exception {
doStartOrResumeRouteConsumers(camelContext, inputs, true, addingRoutes);
}
void doStartRouteConsumers(
AbstractCamelContext camelContext, Map inputs, boolean addingRoutes)
throws Exception {
doStartOrResumeRouteConsumers(camelContext, inputs, false, addingRoutes);
}
private LoggingLevel getRouteLoggerLogLevel(AbstractCamelContext camelContext) {
return camelContext.getRouteController().getLoggingLevel();
}
private void doStartOrResumeRouteConsumers(
AbstractCamelContext camelContext,
Map inputs, boolean resumeOnly, boolean addingRoute)
throws Exception {
List routeInputs = new ArrayList<>();
for (Map.Entry entry : inputs.entrySet()) {
Integer order = entry.getKey();
Route route = entry.getValue().getRoute();
RouteService routeService = entry.getValue().getRouteService();
// if we are starting camel, then skip routes which are configured
// to not be auto started
boolean autoStartup = routeService.isAutoStartup();
if (addingRoute && !autoStartup) {
routeLogger.log(
"Skipping starting of route " + routeService.getId() + " as it's configured with autoStartup=false",
getRouteLoggerLogLevel(camelContext));
continue;
}
StartupStep step = camelContext.getCamelContextExtension().getStartupStepRecorder().beginStep(Route.class,
route.getRouteId(),
"Start Route");
// do some preparation before starting the consumer on the route
Consumer consumer = routeService.getInput();
if (consumer != null) {
Endpoint endpoint = consumer.getEndpoint();
// check multiple consumer violation, with the other routes to be started
if (!doCheckMultipleConsumerSupportClash(endpoint, routeInputs)) {
throw new FailedToStartRouteException(
routeService.getId(), "Multiple consumers for the same endpoint is not allowed: " + endpoint);
}
// check for multiple consumer violations with existing routes
// which have already been started, or is currently starting
List existingEndpoints = new ArrayList<>();
for (Route existingRoute : camelContext.getRoutes()) {
if (route.getId().equals(existingRoute.getId())) {
// skip ourselves
continue;
}
Endpoint existing = existingRoute.getEndpoint();
ServiceStatus status = camelContext.getRouteStatus(existingRoute.getId());
if (status != null && (status.isStarted() || status.isStarting())) {
existingEndpoints.add(existing);
}
}
if (!doCheckMultipleConsumerSupportClash(endpoint, existingEndpoints)) {
throw new FailedToStartRouteException(
routeService.getId(), "Multiple consumers for the same endpoint is not allowed: " + endpoint);
}
// start the consumer on the route
LOG.debug("Route: {} >>> {}", route.getId(), route);
if (resumeOnly) {
LOG.debug("Resuming consumer (order: {}) on route: {}", order, route.getId());
} else {
LOG.debug("Starting consumer (order: {}) on route: {}", order, route.getId());
}
if (resumeOnly && route.supportsSuspension()) {
// if we are resuming and the route can be resumed
ServiceHelper.resumeService(consumer);
// use basic endpoint uri to not log verbose details or potential sensitive data
String uri = endpoint.getEndpointBaseUri();
uri = URISupport.sanitizeUri(uri);
routeLogger.log("Route: " + route.getId() + " resumed and consuming from: " + uri,
getRouteLoggerLogLevel(camelContext));
} else {
// when starting we should invoke the lifecycle strategies
for (LifecycleStrategy strategy : camelContext.getLifecycleStrategies()) {
strategy.onServiceAdd(camelContext.getCamelContextReference(), consumer, route);
}
try {
camelContext.startService(consumer);
route.getProperties().remove("route.start.exception");
} catch (Exception e) {
route.getProperties().put("route.start.exception", e);
throw e;
}
// use basic endpoint uri to not log verbose details or potential sensitive data
String uri = endpoint.getEndpointBaseUri();
uri = URISupport.sanitizeUri(uri);
routeLogger.log("Route: " + route.getId() + " started and consuming from: " + uri,
getRouteLoggerLogLevel(camelContext));
}
routeInputs.add(endpoint);
// add to the order which they was started, so we know how to
// stop them in reverse order
// but only add if we haven't already registered it before (we
// dont want to double add when restarting)
boolean found = false;
for (RouteStartupOrder other : camelContext.getCamelContextExtension().getRouteStartupOrder()) {
if (other.getRoute().getId().equals(route.getId())) {
found = true;
break;
}
}
if (!found) {
camelContext.getCamelContextExtension().getRouteStartupOrder().add(entry.getValue());
}
}
if (resumeOnly) {
routeService.resume();
} else {
// and start the route service (no need to start children as
// they are already warmed up)
try {
routeService.start();
route.getProperties().remove("route.start.exception");
} catch (Exception e) {
route.getProperties().put("route.start.exception", e);
throw e;
}
}
camelContext.getCamelContextExtension().getStartupStepRecorder().endStep(step);
}
}
private boolean doCheckMultipleConsumerSupportClash(Endpoint endpoint, List routeInputs) {
// is multiple consumers supported
boolean multipleConsumersSupported = false;
if (endpoint instanceof MultipleConsumersSupport consumersSupport) {
multipleConsumersSupported = consumersSupport.isMultipleConsumersSupported();
}
if (multipleConsumersSupported) {
// multiple consumer allowed, so return true
return true;
}
// check in progress list
if (routeInputs.contains(endpoint)) {
return false;
}
return true;
}
int incrementRouteStartupOrder() {
return defaultRouteStartupOrder++;
}
}