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;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.HashSet;
024import java.util.LinkedHashMap;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Set;
030import java.util.concurrent.ExecutionException;
031import java.util.concurrent.ExecutorService;
032import java.util.concurrent.Future;
033import java.util.concurrent.TimeUnit;
034import java.util.concurrent.atomic.AtomicBoolean;
035
036import org.apache.camel.CamelContext;
037import org.apache.camel.CamelContextAware;
038import org.apache.camel.Consumer;
039import org.apache.camel.Route;
040import org.apache.camel.Service;
041import org.apache.camel.ShutdownRoute;
042import org.apache.camel.ShutdownRunningTask;
043import org.apache.camel.Suspendable;
044import org.apache.camel.spi.InflightRepository;
045import org.apache.camel.spi.RouteStartupOrder;
046import org.apache.camel.spi.ShutdownAware;
047import org.apache.camel.spi.ShutdownPrepared;
048import org.apache.camel.spi.ShutdownStrategy;
049import org.apache.camel.support.ServiceSupport;
050import org.apache.camel.util.CollectionStringBuffer;
051import org.apache.camel.util.EventHelper;
052import org.apache.camel.util.ObjectHelper;
053import org.apache.camel.util.ServiceHelper;
054import org.apache.camel.util.StopWatch;
055import org.slf4j.Logger;
056import org.slf4j.LoggerFactory;
057
058/**
059 * Default {@link org.apache.camel.spi.ShutdownStrategy} which uses graceful shutdown.
060 * <p/>
061 * Graceful shutdown ensures that any inflight and pending messages will be taken into account
062 * and it will wait until these exchanges has been completed.
063 * <p/>
064 * This strategy will perform graceful shutdown in two steps:
065 * <ul>
066 *     <li>Graceful - By suspending/stopping consumers, and let any in-flight exchanges complete</li>
067 *     <li>Forced - After a given period of time, a timeout occurred and if there are still pending
068 *     exchanges to complete, then a more aggressive forced strategy is performed.</li>
069 * </ul>
070 * The idea by the <tt>graceful</tt> shutdown strategy, is to stop taking in more new messages,
071 * and allow any existing inflight messages to complete. Then when there is no more inflight messages
072 * then the routes can be fully shutdown. This mean that if there is inflight messages then we will have
073 * to wait for these messages to complete. If they do not complete after a period of time, then a
074 * timeout triggers. And then a more aggressive strategy takes over.
075 * <p/>
076 * The idea by the <tt>forced</tt> shutdown strategy, is to stop continue processing messages.
077 * And force routes and its services to shutdown now. There is a risk when shutting down now,
078 * that some resources is not properly shutdown, which can cause side effects. The timeout value
079 * is by default 300 seconds, but can be customized.
080 * <p/>
081 * As this strategy will politely wait until all exchanges has been completed it can potential wait
082 * for a long time, and hence why a timeout value can be set. When the timeout triggers you can also
083 * specify whether the remainder consumers should be shutdown now or ignore.
084 * <p/>
085 * Will by default use a timeout of 300 seconds (5 minutes) by which it will shutdown now the remaining consumers.
086 * This ensures that when shutting down Camel it at some point eventually will shutdown.
087 * This behavior can of course be configured using the {@link #setTimeout(long)} and
088 * {@link #setShutdownNowOnTimeout(boolean)} methods.
089 * <p/>
090 * Routes will by default be shutdown in the reverse order of which they where started.
091 * You can customize this using the {@link #setShutdownRoutesInReverseOrder(boolean)} method.
092 * <p/>
093 * After route consumers have been shutdown, then any {@link ShutdownPrepared} services on the routes
094 * is being prepared for shutdown, by invoking {@link ShutdownPrepared#prepareShutdown(boolean,boolean)} which
095 * <tt>force=false</tt>.
096 * <p/>
097 * Then if a timeout occurred and the strategy has been configured with shutdown-now on timeout, then
098 * the strategy performs a more aggressive forced shutdown, by forcing all consumers to shutdown
099 * and then invokes {@link ShutdownPrepared#prepareShutdown(boolean,boolean)} with <tt>force=true</tt>
100 * on the services. This allows the services to know they should force shutdown now.
101 * <p/>
102 * When timeout occurred and a forced shutdown is happening, then there may be threads/tasks which are
103 * still inflight which may be rejected continued being routed. By default this can cause WARN and ERRORs
104 * to be logged. The option {@link #setSuppressLoggingOnTimeout(boolean)} can be used to suppress these
105 * logs, so they are logged at TRACE level instead.
106 * <p/>
107 * Also when a timeout occurred then information about the inflight exchanges is logged, if {@link #isLogInflightExchangesOnTimeout()}
108 * is enabled (is by default). This allows end users to known where these inflight exchanges currently are in the route(s),
109 * and how long time they have been inflight.
110 * <p/>
111 * This information can also be obtained from the {@link org.apache.camel.spi.InflightRepository}
112 * at all time during runtime.
113 *
114 * @version
115 */
116public class DefaultShutdownStrategy extends ServiceSupport implements ShutdownStrategy, CamelContextAware {
117    private static final Logger LOG = LoggerFactory.getLogger(DefaultShutdownStrategy.class);
118
119    private CamelContext camelContext;
120    private ExecutorService executor;
121    private long timeout = 5 * 60;
122    private TimeUnit timeUnit = TimeUnit.SECONDS;
123    private boolean shutdownNowOnTimeout = true;
124    private boolean shutdownRoutesInReverseOrder = true;
125    private boolean suppressLoggingOnTimeout;
126    private boolean logInflightExchangesOnTimeout = true;
127
128    private volatile boolean forceShutdown;
129    private final AtomicBoolean timeoutOccurred = new AtomicBoolean();
130    private volatile Future<?> currentShutdownTaskFuture;
131
132    public DefaultShutdownStrategy() {
133    }
134
135    public DefaultShutdownStrategy(CamelContext camelContext) {
136        this.camelContext = camelContext;
137    }
138
139    public void shutdown(CamelContext context, List<RouteStartupOrder> routes) throws Exception {
140        shutdown(context, routes, getTimeout(), getTimeUnit());
141    }
142
143    @Override
144    public void shutdownForced(CamelContext context, List<RouteStartupOrder> routes) throws Exception {
145        doShutdown(context, routes, getTimeout(), getTimeUnit(), false, false, true);
146    }
147
148    public void suspend(CamelContext context, List<RouteStartupOrder> routes) throws Exception {
149        doShutdown(context, routes, getTimeout(), getTimeUnit(), true, false, false);
150    }
151
152    public void shutdown(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit) throws Exception {
153        doShutdown(context, routes, timeout, timeUnit, false, false, false);
154    }
155
156    public boolean shutdown(CamelContext context, RouteStartupOrder route, long timeout, TimeUnit timeUnit, boolean abortAfterTimeout) throws Exception {
157        List<RouteStartupOrder> routes = new ArrayList<RouteStartupOrder>(1);
158        routes.add(route);
159        return doShutdown(context, routes, timeout, timeUnit, false, abortAfterTimeout, false);
160    }
161
162    public void suspend(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit) throws Exception {
163        doShutdown(context, routes, timeout, timeUnit, true, false, false);
164    }
165
166    protected boolean doShutdown(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit,
167                                 boolean suspendOnly, boolean abortAfterTimeout, boolean forceShutdown) throws Exception {
168
169        // timeout must be a positive value
170        if (timeout <= 0) {
171            throw new IllegalArgumentException("Timeout must be a positive value");
172        }
173
174        // just return if no routes to shutdown
175        if (routes.isEmpty()) {
176            return true;
177        }
178
179        StopWatch watch = new StopWatch();
180
181        // at first sort according to route startup order
182        List<RouteStartupOrder> routesOrdered = new ArrayList<RouteStartupOrder>(routes);
183        routesOrdered.sort(new Comparator<RouteStartupOrder>() {
184            public int compare(RouteStartupOrder o1, RouteStartupOrder o2) {
185                return o1.getStartupOrder() - o2.getStartupOrder();
186            }
187        });
188        if (shutdownRoutesInReverseOrder) {
189            Collections.reverse(routesOrdered);
190        }
191
192        if (suspendOnly) {
193            LOG.info("Starting to graceful suspend " + routesOrdered.size() + " routes (timeout " + timeout + " " + timeUnit.toString().toLowerCase(Locale.ENGLISH) + ")");
194        } else {
195            LOG.info("Starting to graceful shutdown " + routesOrdered.size() + " routes (timeout " + timeout + " " + timeUnit.toString().toLowerCase(Locale.ENGLISH) + ")");
196        }
197
198        // use another thread to perform the shutdowns so we can support timeout
199        timeoutOccurred.set(false);
200        currentShutdownTaskFuture = getExecutorService().submit(new ShutdownTask(context, routesOrdered, timeout, timeUnit, suspendOnly, abortAfterTimeout, timeoutOccurred));
201        try {
202            currentShutdownTaskFuture.get(timeout, timeUnit);
203        } catch (ExecutionException e) {
204            // unwrap execution exception
205            throw ObjectHelper.wrapRuntimeCamelException(e.getCause());
206        } catch (Exception e) {
207            // either timeout or interrupted exception was thrown so this is okay
208            // as interrupted would mean cancel was called on the currentShutdownTaskFuture to signal a forced timeout
209
210            // we hit a timeout, so set the flag
211            timeoutOccurred.set(true);
212
213            // timeout then cancel the task
214            currentShutdownTaskFuture.cancel(true);
215
216            // signal we are forcing shutdown now, since timeout occurred
217            this.forceShutdown = forceShutdown;
218
219            // if set, stop processing and return false to indicate that the shutdown is aborting
220            if (!forceShutdown && abortAfterTimeout) {
221                LOG.warn("Timeout occurred during graceful shutdown. Aborting the shutdown now."
222                        + " Notice: some resources may still be running as graceful shutdown did not complete successfully.");
223
224                // we attempt to force shutdown so lets log the current inflight exchanges which are affected
225                logInflightExchanges(context, routes, isLogInflightExchangesOnTimeout());
226
227                return false;
228            } else {
229                if (forceShutdown || shutdownNowOnTimeout) {
230                    LOG.warn("Timeout occurred during graceful shutdown. Forcing the routes to be shutdown now."
231                            + " Notice: some resources may still be running as graceful shutdown did not complete successfully.");
232
233                    // we attempt to force shutdown so lets log the current inflight exchanges which are affected
234                    logInflightExchanges(context, routes, isLogInflightExchangesOnTimeout());
235
236                    // force the routes to shutdown now
237                    shutdownRoutesNow(routesOrdered);
238
239                    // now the route consumers has been shutdown, then prepare route services for shutdown now (forced)
240                    for (RouteStartupOrder order : routes) {
241                        for (Service service : order.getServices()) {
242                            prepareShutdown(service, false, true, true, isSuppressLoggingOnTimeout());
243                        }
244                    }
245                } else {
246                    LOG.warn("Timeout occurred during graceful shutdown. Will ignore shutting down the remainder routes."
247                            + " Notice: some resources may still be running as graceful shutdown did not complete successfully.");
248
249                    logInflightExchanges(context, routes, isLogInflightExchangesOnTimeout());
250                }
251            }
252        } finally {
253            currentShutdownTaskFuture = null;
254        }
255
256        // convert to seconds as its easier to read than a big milli seconds number
257        long seconds = TimeUnit.SECONDS.convert(watch.stop(), TimeUnit.MILLISECONDS);
258
259        LOG.info("Graceful shutdown of " + routesOrdered.size() + " routes completed in " + seconds + " seconds");
260        return true;
261    }
262
263    @Override
264    public boolean forceShutdown(Service service) {
265        return forceShutdown;
266    }
267
268    @Override
269    public boolean hasTimeoutOccurred() {
270        return timeoutOccurred.get();
271    }
272
273    public void setTimeout(long timeout) {
274        if (timeout <= 0) {
275            throw new IllegalArgumentException("Timeout must be a positive value");
276        }
277        this.timeout = timeout;
278    }
279
280    public long getTimeout() {
281        return timeout;
282    }
283
284    public void setTimeUnit(TimeUnit timeUnit) {
285        this.timeUnit = timeUnit;
286    }
287
288    public TimeUnit getTimeUnit() {
289        return timeUnit;
290    }
291
292    public void setShutdownNowOnTimeout(boolean shutdownNowOnTimeout) {
293        this.shutdownNowOnTimeout = shutdownNowOnTimeout;
294    }
295
296    public boolean isShutdownNowOnTimeout() {
297        return shutdownNowOnTimeout;
298    }
299
300    public boolean isShutdownRoutesInReverseOrder() {
301        return shutdownRoutesInReverseOrder;
302    }
303
304    public void setShutdownRoutesInReverseOrder(boolean shutdownRoutesInReverseOrder) {
305        this.shutdownRoutesInReverseOrder = shutdownRoutesInReverseOrder;
306    }
307
308    public boolean isSuppressLoggingOnTimeout() {
309        return suppressLoggingOnTimeout;
310    }
311
312    public void setSuppressLoggingOnTimeout(boolean suppressLoggingOnTimeout) {
313        this.suppressLoggingOnTimeout = suppressLoggingOnTimeout;
314    }
315
316    public boolean isLogInflightExchangesOnTimeout() {
317        return logInflightExchangesOnTimeout;
318    }
319
320    public void setLogInflightExchangesOnTimeout(boolean logInflightExchangesOnTimeout) {
321        this.logInflightExchangesOnTimeout = logInflightExchangesOnTimeout;
322    }
323
324    public CamelContext getCamelContext() {
325        return camelContext;
326    }
327
328    public void setCamelContext(CamelContext camelContext) {
329        this.camelContext = camelContext;
330    }
331
332    public Future<?> getCurrentShutdownTaskFuture() {
333        return currentShutdownTaskFuture;
334    }
335
336    /**
337     * Shutdown all the consumers immediately.
338     *
339     * @param routes the routes to shutdown
340     */
341    protected void shutdownRoutesNow(List<RouteStartupOrder> routes) {
342        for (RouteStartupOrder order : routes) {
343
344            // set the route to shutdown as fast as possible by stopping after
345            // it has completed its current task
346            ShutdownRunningTask current = order.getRoute().getRouteContext().getShutdownRunningTask();
347            if (current != ShutdownRunningTask.CompleteCurrentTaskOnly) {
348                LOG.debug("Changing shutdownRunningTask from {} to " +  ShutdownRunningTask.CompleteCurrentTaskOnly
349                    + " on route {} to shutdown faster", current, order.getRoute().getId());
350                order.getRoute().getRouteContext().setShutdownRunningTask(ShutdownRunningTask.CompleteCurrentTaskOnly);
351            }
352
353            for (Consumer consumer : order.getInputs()) {
354                shutdownNow(consumer);
355            }
356        }
357    }
358
359    /**
360     * Shutdown all the consumers immediately.
361     *
362     * @param consumers the consumers to shutdown
363     */
364    protected void shutdownNow(List<Consumer> consumers) {
365        for (Consumer consumer : consumers) {
366            shutdownNow(consumer);
367        }
368    }
369
370    /**
371     * Shutdown the consumer immediately.
372     *
373     * @param consumer the consumer to shutdown
374     */
375    protected static void shutdownNow(Consumer consumer) {
376        LOG.trace("Shutting down: {}", consumer);
377
378        // allow us to do custom work before delegating to service helper
379        try {
380            ServiceHelper.stopService(consumer);
381        } catch (Throwable e) {
382            LOG.warn("Error occurred while shutting down route: " + consumer + ". This exception will be ignored.", e);
383            // fire event
384            EventHelper.notifyServiceStopFailure(consumer.getEndpoint().getCamelContext(), consumer, e);
385        }
386
387        LOG.trace("Shutdown complete for: {}", consumer);
388    }
389
390    /**
391     * Suspends/stops the consumer immediately.
392     *
393     * @param consumer the consumer to suspend
394     */
395    protected static void suspendNow(Consumer consumer) {
396        LOG.trace("Suspending: {}", consumer);
397
398        // allow us to do custom work before delegating to service helper
399        try {
400            ServiceHelper.suspendService(consumer);
401        } catch (Throwable e) {
402            LOG.warn("Error occurred while suspending route: " + consumer + ". This exception will be ignored.", e);
403            // fire event
404            EventHelper.notifyServiceStopFailure(consumer.getEndpoint().getCamelContext(), consumer, e);
405        }
406
407        LOG.trace("Suspend complete for: {}", consumer);
408    }
409
410    private ExecutorService getExecutorService() {
411        if (executor == null) {
412            // use a thread pool that allow to terminate idle threads so they do not hang around forever
413            executor = camelContext.getExecutorServiceManager().newThreadPool(this, "ShutdownTask", 0, 1);
414        }
415        return executor;
416    }
417
418    @Override
419    protected void doStart() throws Exception {
420        ObjectHelper.notNull(camelContext, "CamelContext");
421        // reset option
422        forceShutdown = false;
423        timeoutOccurred.set(false);
424    }
425
426    @Override
427    protected void doStop() throws Exception {
428        // noop
429    }
430
431    @Override
432    protected void doShutdown() throws Exception {
433        if (executor != null) {
434            // force shutting down as we are shutting down Camel
435            camelContext.getExecutorServiceManager().shutdownNow(executor);
436            // should clear executor so we can restart by creating a new thread pool
437            executor = null;
438        }
439    }
440
441    /**
442     * Prepares the services for shutdown, by invoking the {@link ShutdownPrepared#prepareShutdown(boolean, boolean)} method
443     * on the service if it implement this interface.
444     *
445     * @param service the service
446     * @param forced  whether to force shutdown
447     * @param includeChildren whether to prepare the child of the service as well
448     */
449    private static void prepareShutdown(Service service, boolean suspendOnly, boolean forced, boolean includeChildren, boolean suppressLogging) {
450        Set<Service> list;
451        if (includeChildren) {
452            // include error handlers as we want to prepare them for shutdown as well
453            list = ServiceHelper.getChildServices(service, true);
454        } else {
455            list = new LinkedHashSet<Service>(1);
456            list.add(service);
457        }
458
459        for (Service child : list) {
460            if (child instanceof ShutdownPrepared) {
461                try {
462                    LOG.trace("Preparing {} shutdown on {}", forced ? "forced" : "", child);
463                    ((ShutdownPrepared) child).prepareShutdown(suspendOnly, forced);
464                } catch (Exception e) {
465                    if (suppressLogging) {
466                        LOG.trace("Error during prepare shutdown on " + child + ". This exception will be ignored.", e);
467                    } else {
468                        LOG.warn("Error during prepare shutdown on " + child + ". This exception will be ignored.", e);
469                    }
470                }
471            }
472        }
473    }
474
475    /**
476     * Holder for deferred consumers
477     */
478    static class ShutdownDeferredConsumer {
479        private final Route route;
480        private final Consumer consumer;
481
482        ShutdownDeferredConsumer(Route route, Consumer consumer) {
483            this.route = route;
484            this.consumer = consumer;
485        }
486
487        Route getRoute() {
488            return route;
489        }
490
491        Consumer getConsumer() {
492            return consumer;
493        }
494    }
495
496    /**
497     * Shutdown task which shutdown all the routes in a graceful manner.
498     */
499    static class ShutdownTask implements Runnable {
500
501        private final CamelContext context;
502        private final List<RouteStartupOrder> routes;
503        private final boolean suspendOnly;
504        private final boolean abortAfterTimeout;
505        private final long timeout;
506        private final TimeUnit timeUnit;
507        private final AtomicBoolean timeoutOccurred;
508
509        ShutdownTask(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit,
510                            boolean suspendOnly, boolean abortAfterTimeout, AtomicBoolean timeoutOccurred) {
511            this.context = context;
512            this.routes = routes;
513            this.suspendOnly = suspendOnly;
514            this.abortAfterTimeout = abortAfterTimeout;
515            this.timeout = timeout;
516            this.timeUnit = timeUnit;
517            this.timeoutOccurred = timeoutOccurred;
518        }
519
520        public void run() {
521            // the strategy in this run method is to
522            // 1) go over the routes and shutdown those routes which can be shutdown asap
523            //    some routes will be deferred to shutdown at the end, as they are needed
524            //    by other routes so they can complete their tasks
525            // 2) wait until all inflight and pending exchanges has been completed
526            // 3) shutdown the deferred routes
527
528            LOG.debug("There are {} routes to {}", routes.size(), suspendOnly ? "suspend" : "shutdown");
529
530            // list of deferred consumers to shutdown when all exchanges has been completed routed
531            // and thus there are no more inflight exchanges so they can be safely shutdown at that time
532            List<ShutdownDeferredConsumer> deferredConsumers = new ArrayList<ShutdownDeferredConsumer>();
533            for (RouteStartupOrder order : routes) {
534
535                ShutdownRoute shutdownRoute = order.getRoute().getRouteContext().getShutdownRoute();
536                ShutdownRunningTask shutdownRunningTask = order.getRoute().getRouteContext().getShutdownRunningTask();
537
538                if (LOG.isTraceEnabled()) {
539                    LOG.trace("{}{} with options [{},{}]",
540                            new Object[]{suspendOnly ? "Suspending route: " : "Shutting down route: ",
541                                order.getRoute().getId(), shutdownRoute, shutdownRunningTask});
542                }
543
544                for (Consumer consumer : order.getInputs()) {
545
546                    boolean suspend = false;
547
548                    // assume we should shutdown if we are not deferred
549                    boolean shutdown = shutdownRoute != ShutdownRoute.Defer;
550
551                    if (shutdown) {
552                        // if we are to shutdown then check whether we can suspend instead as its a more
553                        // gentle way to graceful shutdown
554
555                        // some consumers do not support shutting down so let them decide
556                        // if a consumer is suspendable then prefer to use that and then shutdown later
557                        if (consumer instanceof ShutdownAware) {
558                            shutdown = !((ShutdownAware) consumer).deferShutdown(shutdownRunningTask);
559                        }
560                        if (shutdown && consumer instanceof Suspendable) {
561                            // we prefer to suspend over shutdown
562                            suspend = true;
563                        }
564                    }
565
566                    // log at info level when a route has been shutdown (otherwise log at debug level to not be too noisy)
567                    if (suspend) {
568                        // only suspend it and then later shutdown it
569                        suspendNow(consumer);
570                        // add it to the deferred list so the route will be shutdown later
571                        deferredConsumers.add(new ShutdownDeferredConsumer(order.getRoute(), consumer));
572                        LOG.debug("Route: {} suspended and shutdown deferred, was consuming from: {}", order.getRoute().getId(), order.getRoute().getEndpoint());
573                    } else if (shutdown) {
574                        shutdownNow(consumer);
575                        LOG.info("Route: {} shutdown complete, was consuming from: {}", order.getRoute().getId(), order.getRoute().getEndpoint());
576                    } else {
577                        // we will stop it later, but for now it must run to be able to help all inflight messages
578                        // be safely completed
579                        deferredConsumers.add(new ShutdownDeferredConsumer(order.getRoute(), consumer));
580                        LOG.debug("Route: " + order.getRoute().getId() + (suspendOnly ? " shutdown deferred." : " suspension deferred."));
581                    }
582                }
583            }
584
585            // notify the services we intend to shutdown
586            for (RouteStartupOrder order : routes) {
587                for (Service service : order.getServices()) {
588                    // skip the consumer as we handle that specially
589                    if (service instanceof Consumer) {
590                        continue;
591                    }
592                    prepareShutdown(service, suspendOnly, false, true, false);
593                }
594            }
595
596            // wait till there are no more pending and inflight messages
597            boolean done = false;
598            long loopDelaySeconds = 1;
599            long loopCount = 0;
600            while (!done && !timeoutOccurred.get()) {
601                int size = 0;
602                // number of inflights per route
603                final Map<String, Integer> routeInflight = new LinkedHashMap<String, Integer>();
604
605                for (RouteStartupOrder order : routes) {
606                    int inflight = context.getInflightRepository().size(order.getRoute().getId());
607                    inflight += getPendingInflightExchanges(order);
608                    if (inflight > 0) {
609                        String routeId = order.getRoute().getId();
610                        routeInflight.put(routeId, inflight);
611                        size += inflight;
612                        LOG.trace("{} inflight and pending exchanges for route: {}", inflight, routeId);
613                    }
614                }
615                if (size > 0) {
616                    try {
617                        // build a message with inflight per route
618                        CollectionStringBuffer csb = new CollectionStringBuffer();
619                        for (Map.Entry<String, Integer> entry : routeInflight.entrySet()) {
620                            String row = String.format("%s = %s", entry.getKey(), entry.getValue());
621                            csb.append(row);
622                        }
623
624                        String msg = "Waiting as there are still " + size + " inflight and pending exchanges to complete, timeout in "
625                                + (TimeUnit.SECONDS.convert(timeout, timeUnit) - (loopCount++ * loopDelaySeconds)) + " seconds.";
626                        msg += " Inflights per route: [" + csb.toString() + "]";
627
628                        LOG.info(msg);
629
630                        // log verbose if DEBUG logging is enabled
631                        logInflightExchanges(context, routes, false);
632
633                        Thread.sleep(loopDelaySeconds * 1000);
634                    } catch (InterruptedException e) {
635                        if (abortAfterTimeout) {
636                            LOG.warn("Interrupted while waiting during graceful shutdown, will abort.");
637                            return;
638                        } else {
639                            LOG.warn("Interrupted while waiting during graceful shutdown, will force shutdown now.");
640                            break;
641                        }
642                    }
643                } else {
644                    done = true;
645                }
646            }
647
648            // prepare for shutdown
649            for (ShutdownDeferredConsumer deferred : deferredConsumers) {
650                Consumer consumer = deferred.getConsumer();
651                if (consumer instanceof ShutdownAware) {
652                    LOG.trace("Route: {} preparing to shutdown.", deferred.getRoute().getId());
653                    boolean forced = context.getShutdownStrategy().forceShutdown(consumer);
654                    boolean suppress = context.getShutdownStrategy().isSuppressLoggingOnTimeout();
655                    prepareShutdown(consumer, suspendOnly, forced, false, suppress);
656                    LOG.debug("Route: {} preparing to shutdown complete.", deferred.getRoute().getId());
657                }
658            }
659
660            // now all messages has been completed then stop the deferred consumers
661            for (ShutdownDeferredConsumer deferred : deferredConsumers) {
662                Consumer consumer = deferred.getConsumer();
663                if (suspendOnly) {
664                    suspendNow(consumer);
665                    LOG.info("Route: {} suspend complete, was consuming from: {}", deferred.getRoute().getId(), deferred.getConsumer().getEndpoint());
666                } else {
667                    shutdownNow(consumer);
668                    LOG.info("Route: {} shutdown complete, was consuming from: {}", deferred.getRoute().getId(), deferred.getConsumer().getEndpoint());
669                }
670            }
671
672            // now the route consumers has been shutdown, then prepare route services for shutdown
673            for (RouteStartupOrder order : routes) {
674                for (Service service : order.getServices()) {
675                    boolean forced = context.getShutdownStrategy().forceShutdown(service);
676                    boolean suppress = context.getShutdownStrategy().isSuppressLoggingOnTimeout();
677                    prepareShutdown(service, suspendOnly, forced, true, suppress);
678                }
679            }
680        }
681
682    }
683
684    /**
685     * Calculates the total number of inflight exchanges for the given route
686     *
687     * @param order the route
688     * @return number of inflight exchanges
689     */
690    protected static int getPendingInflightExchanges(RouteStartupOrder order) {
691        int inflight = 0;
692
693        // the consumer is the 1st service so we always get the consumer
694        // the child services are EIPs in the routes which may also have pending
695        // inflight exchanges (such as the aggregator)
696        for (Service service : order.getServices()) {
697            Set<Service> children = ServiceHelper.getChildServices(service);
698            for (Service child : children) {
699                if (child instanceof ShutdownAware) {
700                    inflight += ((ShutdownAware) child).getPendingExchangesSize();
701                }
702            }
703        }
704
705        return inflight;
706    }
707
708    /**
709     * Logs information about the inflight exchanges
710     *
711     * @param infoLevel <tt>true</tt> to log at INFO level, <tt>false</tt> to log at DEBUG level
712     */
713    protected static void logInflightExchanges(CamelContext camelContext, List<RouteStartupOrder> routes, boolean infoLevel) {
714        // check if we need to log
715        if (!infoLevel && !LOG.isDebugEnabled()) {
716            return;
717        }
718
719        Collection<InflightRepository.InflightExchange> inflights = camelContext.getInflightRepository().browse();
720        int size = inflights.size();
721        if (size == 0) {
722            return;
723        }
724
725        // filter so inflight must start from any of the routes
726        Set<String> routeIds = new HashSet<String>();
727        for (RouteStartupOrder route : routes) {
728            routeIds.add(route.getRoute().getId());
729        }
730        Collection<InflightRepository.InflightExchange> filtered = new ArrayList<InflightRepository.InflightExchange>();
731        for (InflightRepository.InflightExchange inflight : inflights) {
732            String routeId = inflight.getExchange().getFromRouteId();
733            if (routeIds.contains(routeId)) {
734                filtered.add(inflight);
735            }
736        }
737
738        size = filtered.size();
739        if (size == 0) {
740            return;
741        }
742
743        StringBuilder sb = new StringBuilder("There are " + size + " inflight exchanges:");
744        for (InflightRepository.InflightExchange inflight : filtered) {
745            sb.append("\n\tInflightExchange: [exchangeId=").append(inflight.getExchange().getExchangeId())
746                    .append(", fromRouteId=").append(inflight.getExchange().getFromRouteId())
747                    .append(", routeId=").append(inflight.getRouteId())
748                    .append(", nodeId=").append(inflight.getNodeId())
749                    .append(", elapsed=").append(inflight.getElapsed())
750                    .append(", duration=").append(inflight.getDuration())
751                    .append("]");
752        }
753
754        if (infoLevel) {
755            LOG.info(sb.toString());
756        } else {
757            LOG.debug(sb.toString());
758        }
759    }
760
761}