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     */
017    package org.apache.camel.impl;
018    
019    import java.util.ArrayList;
020    import java.util.Collections;
021    import java.util.Comparator;
022    import java.util.List;
023    import java.util.Locale;
024    import java.util.concurrent.ExecutionException;
025    import java.util.concurrent.ExecutorService;
026    import java.util.concurrent.Future;
027    import java.util.concurrent.TimeUnit;
028    import java.util.concurrent.TimeoutException;
029    
030    import org.apache.camel.CamelContext;
031    import org.apache.camel.CamelContextAware;
032    import org.apache.camel.Consumer;
033    import org.apache.camel.Route;
034    import org.apache.camel.Service;
035    import org.apache.camel.ShutdownRoute;
036    import org.apache.camel.ShutdownRunningTask;
037    import org.apache.camel.SuspendableService;
038    import org.apache.camel.spi.RouteStartupOrder;
039    import org.apache.camel.spi.ShutdownAware;
040    import org.apache.camel.spi.ShutdownStrategy;
041    import org.apache.camel.support.ServiceSupport;
042    import org.apache.camel.util.EventHelper;
043    import org.apache.camel.util.ObjectHelper;
044    import org.apache.camel.util.ServiceHelper;
045    import org.apache.camel.util.StopWatch;
046    import org.slf4j.Logger;
047    import org.slf4j.LoggerFactory;
048    
049    /**
050     * Default {@link org.apache.camel.spi.ShutdownStrategy} which uses graceful shutdown.
051     * <p/>
052     * Graceful shutdown ensures that any inflight and pending messages will be taken into account
053     * and it will wait until these exchanges has been completed.
054     * <p/>
055     * As this strategy will politely wait until all exchanges has been completed it can potential wait
056     * for a long time, and hence why a timeout value can be set. When the timeout triggers you can also
057     * specify whether the remainder consumers should be shutdown now or ignore.
058     * <p/>
059     * Will by default use a timeout of 300 seconds (5 minutes) by which it will shutdown now the remaining consumers.
060     * This ensures that when shutting down Camel it at some point eventually will shutdown.
061     * This behavior can of course be configured using the {@link #setTimeout(long)} and
062     * {@link #setShutdownNowOnTimeout(boolean)} methods.
063     * <p/>
064     * Routes will by default be shutdown in the reverse order of which they where started.
065     * You can customize this using the {@link #setShutdownRoutesInReverseOrder(boolean)} method.
066     *
067     * @version 
068     */
069    public class DefaultShutdownStrategy extends ServiceSupport implements ShutdownStrategy, CamelContextAware {
070        private static final transient Logger LOG = LoggerFactory.getLogger(DefaultShutdownStrategy.class);
071    
072        private CamelContext camelContext;
073        private ExecutorService executor;
074        private long timeout = 5 * 60;
075        private TimeUnit timeUnit = TimeUnit.SECONDS;
076        private boolean shutdownNowOnTimeout = true;
077        private boolean shutdownRoutesInReverseOrder = true;
078        private volatile boolean forceShutdown;
079    
080        public DefaultShutdownStrategy() {
081        }
082    
083        public DefaultShutdownStrategy(CamelContext camelContext) {
084            this.camelContext = camelContext;
085        }
086    
087        public void shutdown(CamelContext context, List<RouteStartupOrder> routes) throws Exception {
088            shutdown(context, routes, getTimeout(), getTimeUnit());
089        }
090    
091        @Override
092        public void shutdownForced(CamelContext context, List<RouteStartupOrder> routes) throws Exception {
093            doShutdown(context, routes, getTimeout(), getTimeUnit(), false, false, true);
094        }
095    
096        public void suspend(CamelContext context, List<RouteStartupOrder> routes) throws Exception {
097            doShutdown(context, routes, getTimeout(), getTimeUnit(), true, false, false);
098        }
099    
100        public void shutdown(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit) throws Exception {
101            doShutdown(context, routes, timeout, timeUnit, false, false, false);
102        }
103    
104        public boolean shutdown(CamelContext context, RouteStartupOrder route, long timeout, TimeUnit timeUnit, boolean abortAfterTimeout) throws Exception {
105            List<RouteStartupOrder> routes = new ArrayList<RouteStartupOrder>(1);
106            routes.add(route);
107            return doShutdown(context, routes, timeout, timeUnit, false, abortAfterTimeout, false);
108        }
109    
110        public void suspend(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit) throws Exception {
111            doShutdown(context, routes, timeout, timeUnit, true, false, false);
112        }
113    
114        protected boolean doShutdown(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit,
115                                     boolean suspendOnly, boolean abortAfterTimeout, boolean forceShutdown) throws Exception {
116    
117            StopWatch watch = new StopWatch();
118    
119            // at first sort according to route startup order
120            List<RouteStartupOrder> routesOrdered = new ArrayList<RouteStartupOrder>(routes);
121            Collections.sort(routesOrdered, new Comparator<RouteStartupOrder>() {
122                public int compare(RouteStartupOrder o1, RouteStartupOrder o2) {
123                    return o1.getStartupOrder() - o2.getStartupOrder();
124                }
125            });
126            if (shutdownRoutesInReverseOrder) {
127                Collections.reverse(routesOrdered);
128            }
129    
130            if (timeout > 0) {
131                LOG.info("Starting to graceful shutdown " + routesOrdered.size() + " routes (timeout " + timeout + " " + timeUnit.toString().toLowerCase(Locale.ENGLISH) + ")");
132            } else {
133                LOG.info("Starting to graceful shutdown " + routesOrdered.size() + " routes (no timeout)");
134            }
135    
136            // use another thread to perform the shutdowns so we can support timeout
137            Future future = getExecutorService().submit(new ShutdownTask(context, routesOrdered, timeout, timeUnit, suspendOnly, abortAfterTimeout));
138            try {
139                if (timeout > 0) {
140                    future.get(timeout, timeUnit);
141                } else {
142                    future.get();
143                }
144            } catch (TimeoutException e) {
145                // timeout then cancel the task
146                future.cancel(true);
147    
148                // signal we are forcing shutdown now, since timeout occurred
149                this.forceShutdown = forceShutdown;
150    
151                // if set, stop processing and return false to indicate that the shutdown is aborting
152                if (!forceShutdown && abortAfterTimeout) {
153                    LOG.warn("Timeout occurred. Aborting the shutdown now.");
154                    return false;
155                } else {
156                    if (forceShutdown || shutdownNowOnTimeout) {
157                        LOG.warn("Timeout occurred. Now forcing the routes to be shutdown now.");
158                        // force the routes to shutdown now
159                        shutdownRoutesNow(routesOrdered);
160                    } else {
161                        LOG.warn("Timeout occurred. Will ignore shutting down the remainder routes.");
162                    }
163                }
164            } catch (ExecutionException e) {
165                // unwrap execution exception
166                throw ObjectHelper.wrapRuntimeCamelException(e.getCause());
167            }
168    
169            // convert to seconds as its easier to read than a big milli seconds number
170            long seconds = TimeUnit.SECONDS.convert(watch.stop(), TimeUnit.MILLISECONDS);
171    
172            LOG.info("Graceful shutdown of " + routesOrdered.size() + " routes completed in " + seconds + " seconds");
173            return true;
174        }
175    
176        @Override
177        public boolean forceShutdown(Service service) {
178            return forceShutdown;
179        }
180    
181        public void setTimeout(long timeout) {
182            this.timeout = timeout;
183        }
184    
185        public long getTimeout() {
186            return timeout;
187        }
188    
189        public void setTimeUnit(TimeUnit timeUnit) {
190            this.timeUnit = timeUnit;
191        }
192    
193        public TimeUnit getTimeUnit() {
194            return timeUnit;
195        }
196    
197        public void setShutdownNowOnTimeout(boolean shutdownNowOnTimeout) {
198            this.shutdownNowOnTimeout = shutdownNowOnTimeout;
199        }
200    
201        public boolean isShutdownNowOnTimeout() {
202            return shutdownNowOnTimeout;
203        }
204    
205        public boolean isShutdownRoutesInReverseOrder() {
206            return shutdownRoutesInReverseOrder;
207        }
208    
209        public void setShutdownRoutesInReverseOrder(boolean shutdownRoutesInReverseOrder) {
210            this.shutdownRoutesInReverseOrder = shutdownRoutesInReverseOrder;
211        }
212    
213        public CamelContext getCamelContext() {
214            return camelContext;
215        }
216    
217        public void setCamelContext(CamelContext camelContext) {
218            this.camelContext = camelContext;
219        }
220    
221        /**
222         * Shutdown all the consumers immediately.
223         *
224         * @param routes the routes to shutdown
225         */
226        protected void shutdownRoutesNow(List<RouteStartupOrder> routes) {
227            for (RouteStartupOrder order : routes) {
228    
229                // set the route to shutdown as fast as possible by stopping after
230                // it has completed its current task
231                ShutdownRunningTask current = order.getRoute().getRouteContext().getShutdownRunningTask();
232                if (current != ShutdownRunningTask.CompleteCurrentTaskOnly) {
233                    LOG.debug("Changing shutdownRunningTask from {} to " +  ShutdownRunningTask.CompleteCurrentTaskOnly
234                        + " on route {} to shutdown faster", current, order.getRoute().getId());
235                    order.getRoute().getRouteContext().setShutdownRunningTask(ShutdownRunningTask.CompleteCurrentTaskOnly);
236                }
237    
238                for (Consumer consumer : order.getInputs()) {
239                    shutdownNow(consumer);
240                }
241            }
242        }
243    
244        /**
245         * Shutdown all the consumers immediately.
246         *
247         * @param consumers the consumers to shutdown
248         */
249        protected void shutdownNow(List<Consumer> consumers) {
250            for (Consumer consumer : consumers) {
251                shutdownNow(consumer);
252            }
253        }
254    
255        /**
256         * Shutdown the consumer immediately.
257         *
258         * @param consumer the consumer to shutdown
259         */
260        protected static void shutdownNow(Consumer consumer) {
261            LOG.trace("Shutting down: {}", consumer);
262    
263            // allow us to do custom work before delegating to service helper
264            try {
265                ServiceHelper.stopService(consumer);
266            } catch (Throwable e) {
267                LOG.warn("Error occurred while shutting down route: " + consumer + ". This exception will be ignored.", e);
268                // fire event
269                EventHelper.notifyServiceStopFailure(consumer.getEndpoint().getCamelContext(), consumer, e);
270            }
271    
272            LOG.trace("Shutdown complete for: {}", consumer);
273        }
274    
275        /**
276         * Suspends/stops the consumer immediately.
277         *
278         * @param consumer the consumer to suspend
279         */
280        protected static void suspendNow(Consumer consumer) {
281            LOG.trace("Suspending: {}", consumer);
282    
283            // allow us to do custom work before delegating to service helper
284            try {
285                ServiceHelper.suspendService(consumer);
286            } catch (Throwable e) {
287                LOG.warn("Error occurred while suspending route: " + consumer + ". This exception will be ignored.", e);
288                // fire event
289                EventHelper.notifyServiceStopFailure(consumer.getEndpoint().getCamelContext(), consumer, e);
290            }
291    
292            LOG.trace("Suspend complete for: {}", consumer);
293        }
294    
295        private ExecutorService getExecutorService() {
296            if (executor == null) {
297                executor = camelContext.getExecutorServiceManager().newSingleThreadExecutor(this, "ShutdownTask");
298            }
299            return executor;
300        }
301    
302        @Override
303        protected void doStart() throws Exception {
304            ObjectHelper.notNull(camelContext, "CamelContext");
305            // reset option
306            forceShutdown = false;
307        }
308    
309        @Override
310        protected void doStop() throws Exception {
311            // noop
312        }
313    
314        @Override
315        protected void doShutdown() throws Exception {
316            if (executor != null) {
317                camelContext.getExecutorServiceManager().shutdownNow(executor);
318                // should clear executor so we can restart by creating a new thread pool
319                executor = null;
320            }
321        }
322    
323        static class ShutdownDeferredConsumer {
324            private final Route route;
325            private final Consumer consumer;
326    
327            ShutdownDeferredConsumer(Route route, Consumer consumer) {
328                this.route = route;
329                this.consumer = consumer;
330            }
331    
332            Route getRoute() {
333                return route;
334            }
335    
336            Consumer getConsumer() {
337                return consumer;
338            }
339        }
340    
341        /**
342         * Shutdown task which shutdown all the routes in a graceful manner.
343         */
344        static class ShutdownTask implements Runnable {
345    
346            private final CamelContext context;
347            private final List<RouteStartupOrder> routes;
348            private final boolean suspendOnly;
349            private final boolean abortAfterTimeout;
350            private final long timeout;
351            private final TimeUnit timeUnit;
352    
353            public ShutdownTask(CamelContext context, List<RouteStartupOrder> routes, long timeout, TimeUnit timeUnit,
354                                boolean suspendOnly, boolean abortAfterTimeout) {
355                this.context = context;
356                this.routes = routes;
357                this.suspendOnly = suspendOnly;
358                this.abortAfterTimeout = abortAfterTimeout;
359                this.timeout = timeout;
360                this.timeUnit = timeUnit;
361            }
362    
363            public void run() {
364                // the strategy in this run method is to
365                // 1) go over the routes and shutdown those routes which can be shutdown asap
366                //    some routes will be deferred to shutdown at the end, as they are needed
367                //    by other routes so they can complete their tasks
368                // 2) wait until all inflight and pending exchanges has been completed
369                // 3) shutdown the deferred routes
370    
371                LOG.debug("There are {} routes to {}", routes.size(), suspendOnly ? "suspend" : "shutdown");
372    
373                // list of deferred consumers to shutdown when all exchanges has been completed routed
374                // and thus there are no more inflight exchanges so they can be safely shutdown at that time
375                List<ShutdownDeferredConsumer> deferredConsumers = new ArrayList<ShutdownDeferredConsumer>();
376    
377                for (RouteStartupOrder order : routes) {
378    
379                    ShutdownRoute shutdownRoute = order.getRoute().getRouteContext().getShutdownRoute();
380                    ShutdownRunningTask shutdownRunningTask = order.getRoute().getRouteContext().getShutdownRunningTask();
381    
382                    if (LOG.isTraceEnabled()) {
383                        LOG.trace("{}{} with options [{},{}]",
384                                new Object[]{suspendOnly ? "Suspending route: " : "Shutting down route: ",
385                                    order.getRoute().getId(), shutdownRoute, shutdownRunningTask});
386                    }
387    
388                    for (Consumer consumer : order.getInputs()) {
389    
390                        boolean suspend = false;
391    
392                        // assume we should shutdown if we are not deferred
393                        boolean shutdown = shutdownRoute != ShutdownRoute.Defer;
394    
395                        if (shutdown) {
396                            // if we are to shutdown then check whether we can suspend instead as its a more
397                            // gentle way to graceful shutdown
398    
399                            // some consumers do not support shutting down so let them decide
400                            // if a consumer is suspendable then prefer to use that and then shutdown later
401                            if (consumer instanceof ShutdownAware) {
402                                shutdown = !((ShutdownAware) consumer).deferShutdown(shutdownRunningTask);
403                            }
404                            if (shutdown && consumer instanceof SuspendableService) {
405                                // we prefer to suspend over shutdown
406                                suspend = true;
407                            }
408                        }
409    
410                        // log at info level when a route has been shutdown (otherwise log at debug level to not be too noisy)
411                        if (suspend) {
412                            // only suspend it and then later shutdown it
413                            suspendNow(consumer);
414                            // add it to the deferred list so the route will be shutdown later
415                            deferredConsumers.add(new ShutdownDeferredConsumer(order.getRoute(), consumer));
416                            LOG.debug("Route: {} suspended and shutdown deferred, was consuming from: {}", order.getRoute().getId(), order.getRoute().getEndpoint());
417                        } else if (shutdown) {
418                            shutdownNow(consumer);
419                            LOG.info("Route: {} shutdown complete, was consuming from: {}", order.getRoute().getId(), order.getRoute().getEndpoint());
420                        } else {
421                            // we will stop it later, but for now it must run to be able to help all inflight messages
422                            // be safely completed
423                            deferredConsumers.add(new ShutdownDeferredConsumer(order.getRoute(), consumer));
424                            LOG.debug("Route: " + order.getRoute().getId() + (suspendOnly ? " shutdown deferred." : " suspension deferred."));
425                        }
426                    }
427                }
428    
429                // wait till there are no more pending and inflight messages
430                boolean done = false;
431                long loopDelaySeconds = 1;
432                long loopCount = 0;
433                while (!done) {
434                    int size = 0;
435                    for (RouteStartupOrder order : routes) {
436                        int inflight = context.getInflightRepository().size(order.getRoute().getId());
437                        for (Consumer consumer : order.getInputs()) {
438                            // include any additional pending exchanges on some consumers which may have internal
439                            // memory queues such as seda
440                            if (consumer instanceof ShutdownAware) {
441                                inflight += ((ShutdownAware) consumer).getPendingExchangesSize();
442                            }
443                        }
444                        if (inflight > 0) {
445                            size += inflight;
446                            LOG.trace("{} inflight and pending exchanges for route: {}", inflight, order.getRoute().getId());
447                        }
448                    }
449                    if (size > 0) {
450                        try {
451                            LOG.info("Waiting as there are still " + size + " inflight and pending exchanges to complete, timeout in "
452                                     + (TimeUnit.SECONDS.convert(timeout, timeUnit) - (loopCount++ * loopDelaySeconds)) + " seconds.");
453                            Thread.sleep(loopDelaySeconds * 1000);
454                        } catch (InterruptedException e) {
455                            if (abortAfterTimeout) {
456                                LOG.warn("Interrupted while waiting during graceful shutdown, will abort.");
457                                return;
458                            } else {
459                                LOG.warn("Interrupted while waiting during graceful shutdown, will force shutdown now.");
460                                break;
461                            }
462                        }
463                    } else {
464                        done = true;
465                    }
466                }
467    
468                // prepare for shutdown
469                for (ShutdownDeferredConsumer deferred : deferredConsumers) {
470                    Consumer consumer = deferred.getConsumer();
471                    if (consumer instanceof ShutdownAware) {
472                        LOG.trace("Route: {} preparing to shutdown.", deferred.getRoute().getId());
473                        ((ShutdownAware) consumer).prepareShutdown();
474                        LOG.debug("Route: {} preparing to shutdown complete.", deferred.getRoute().getId());
475                    }
476                }
477    
478                // now all messages has been completed then stop the deferred consumers
479                for (ShutdownDeferredConsumer deferred : deferredConsumers) {
480                    Consumer consumer = deferred.getConsumer();
481                    if (suspendOnly) {
482                        suspendNow(consumer);
483                        LOG.info("Route: {} suspend complete, was consuming from: {}", deferred.getRoute().getId(), deferred.getConsumer().getEndpoint());
484                    } else {
485                        shutdownNow(consumer);
486                        LOG.info("Route: {} shutdown complete, was consuming from: {}", deferred.getRoute().getId(), deferred.getConsumer().getEndpoint());
487                    }
488                }
489            }
490    
491        }
492    
493    }