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.Locale;
020    import java.util.concurrent.ScheduledExecutorService;
021    import java.util.concurrent.ScheduledFuture;
022    import java.util.concurrent.TimeUnit;
023    
024    import org.apache.camel.Endpoint;
025    import org.apache.camel.Exchange;
026    import org.apache.camel.LoggingLevel;
027    import org.apache.camel.PollingConsumerPollingStrategy;
028    import org.apache.camel.Processor;
029    import org.apache.camel.SuspendableService;
030    import org.apache.camel.spi.PollingConsumerPollStrategy;
031    import org.apache.camel.util.ObjectHelper;
032    import org.apache.camel.util.ServiceHelper;
033    import org.slf4j.Logger;
034    import org.slf4j.LoggerFactory;
035    
036    /**
037     * A useful base class for any consumer which is polling based
038     * 
039     * @version 
040     */
041    public abstract class ScheduledPollConsumer extends DefaultConsumer implements Runnable, SuspendableService, PollingConsumerPollingStrategy {
042        private static final transient Logger LOG = LoggerFactory.getLogger(ScheduledPollConsumer.class);
043    
044        private ScheduledExecutorService scheduledExecutorService;
045        private boolean shutdownExecutor;
046        private ScheduledFuture<?> future;
047    
048        // if adding more options then align with ScheduledPollEndpoint#configureScheduledPollConsumerProperties
049        private boolean startScheduler = true;
050        private long initialDelay = 1000;
051        private long delay = 500;
052        private TimeUnit timeUnit = TimeUnit.MILLISECONDS;
053        private boolean useFixedDelay = true;
054        private PollingConsumerPollStrategy pollStrategy = new DefaultPollingConsumerPollStrategy();
055        private LoggingLevel runLoggingLevel = LoggingLevel.TRACE;
056        private boolean sendEmptyMessageWhenIdle;
057        private volatile boolean polling;
058    
059        public ScheduledPollConsumer(Endpoint endpoint, Processor processor) {
060            super(endpoint, processor);
061        }
062    
063        public ScheduledPollConsumer(Endpoint endpoint, Processor processor, ScheduledExecutorService scheduledExecutorService) {
064            super(endpoint, processor);
065            // we have been given an existing thread pool, so we should not manage its lifecycle
066            // so we should keep shutdownExecutor as false
067            this.scheduledExecutorService = scheduledExecutorService;
068            ObjectHelper.notNull(scheduledExecutorService, "scheduledExecutorService");
069        }
070    
071        /**
072         * Invoked whenever we should be polled
073         */
074        public void run() {
075            // avoid this thread to throw exceptions because the thread pool wont re-schedule a new thread
076            try {
077                // log starting
078                if (LoggingLevel.ERROR == runLoggingLevel) {
079                    LOG.error("Scheduled task started on:   {}", this.getEndpoint());
080                } else if (LoggingLevel.WARN == runLoggingLevel) {
081                    LOG.warn("Scheduled task started on:   {}", this.getEndpoint());
082                } else if (LoggingLevel.INFO == runLoggingLevel) {
083                    LOG.info("Scheduled task started on:   {}", this.getEndpoint());
084                } else if (LoggingLevel.DEBUG == runLoggingLevel) {
085                    LOG.debug("Scheduled task started on:   {}", this.getEndpoint());
086                } else {
087                    LOG.trace("Scheduled task started on:   {}", this.getEndpoint());
088                }
089    
090                // execute scheduled task
091                doRun();
092    
093                // log completed
094                if (LoggingLevel.ERROR == runLoggingLevel) {
095                    LOG.error("Scheduled task completed on: {}", this.getEndpoint());
096                } else if (LoggingLevel.WARN == runLoggingLevel) {
097                    LOG.warn("Scheduled task completed on: {}", this.getEndpoint());
098                } else if (LoggingLevel.INFO == runLoggingLevel) {
099                    LOG.info("Scheduled task completed on: {}", this.getEndpoint());
100                } else if (LoggingLevel.DEBUG == runLoggingLevel) {
101                    LOG.debug("Scheduled task completed on: {}", this.getEndpoint());
102                } else {
103                    LOG.trace("Scheduled task completed on: {}", this.getEndpoint());
104                }
105    
106            } catch (Error e) {
107                // must catch Error, to ensure the task is re-scheduled
108                LOG.error("Error occurred during running scheduled task on: " + this.getEndpoint() + ", due: " + e.getMessage(), e);
109            }
110        }
111    
112        private void doRun() {
113            if (isSuspended()) {
114                LOG.trace("Cannot start to poll: {} as its suspended", this.getEndpoint());
115                return;
116            }
117    
118            int retryCounter = -1;
119            boolean done = false;
120    
121            while (!done) {
122                try {
123                    // eager assume we are done
124                    done = true;
125                    if (isPollAllowed()) {
126    
127                        if (retryCounter == -1) {
128                            LOG.trace("Starting to poll: {}", this.getEndpoint());
129                        } else {
130                            LOG.debug("Retrying attempt {} to poll: {}", retryCounter, this.getEndpoint());
131                        }
132    
133                        // mark we are polling which should also include the begin/poll/commit
134                        polling = true;
135                        try {
136                            boolean begin = pollStrategy.begin(this, getEndpoint());
137                            if (begin) {
138                                retryCounter++;
139                                int polledMessages = poll();
140    
141                                if (polledMessages == 0 && isSendEmptyMessageWhenIdle()) {
142                                    // send an "empty" exchange
143                                    processEmptyMessage();
144                                }
145    
146                                pollStrategy.commit(this, getEndpoint(), polledMessages);
147                            } else {
148                                LOG.debug("Cannot begin polling as pollStrategy returned false: {}", pollStrategy);
149                            }
150                        } finally {
151                            polling = false;
152                        }
153                    }
154    
155                    LOG.trace("Finished polling: {}", this.getEndpoint());
156                } catch (Exception e) {
157                    try {
158                        boolean retry = pollStrategy.rollback(this, getEndpoint(), retryCounter, e);
159                        if (retry) {
160                            done = false;
161                        }
162                    } catch (Throwable t) {
163                        // catch throwable to not let the thread die
164                        getExceptionHandler().handleException("Consumer " + this +  " failed polling endpoint: " + getEndpoint()
165                                + ". Will try again at next poll", t);
166                        // we are done due this fatal error
167                        done = true;
168                    }
169                } catch (Throwable t) {
170                    // catch throwable to not let the thread die
171                    getExceptionHandler().handleException("Consumer " + this +  " failed polling endpoint: " + getEndpoint()
172                            + ". Will try again at next poll", t);
173                    // we are done due this fatal error
174                    done = true;
175                }
176            }
177    
178            // avoid this thread to throw exceptions because the thread pool wont re-schedule a new thread
179        }
180    
181        /**
182         * No messages to poll so send an empty message instead.
183         *
184         * @throws Exception is thrown if error processing the empty message.
185         */
186        protected void processEmptyMessage() throws Exception {
187            Exchange exchange = getEndpoint().createExchange();
188            log.debug("Sending empty message as there were no messages from polling: {}", this.getEndpoint());
189            getProcessor().process(exchange);
190        }
191    
192        // Properties
193        // -------------------------------------------------------------------------
194    
195        protected boolean isPollAllowed() {
196            return isRunAllowed() && !isSuspended();
197        }
198    
199        /**
200         * Whether polling is currently in progress
201         */
202        protected boolean isPolling() {
203            return polling;
204        }
205    
206        public long getInitialDelay() {
207            return initialDelay;
208        }
209    
210        public void setInitialDelay(long initialDelay) {
211            this.initialDelay = initialDelay;
212        }
213    
214        public long getDelay() {
215            return delay;
216        }
217    
218        public void setDelay(long delay) {
219            this.delay = delay;
220        }
221    
222        public TimeUnit getTimeUnit() {
223            return timeUnit;
224        }
225    
226        /**
227         * Sets the time unit to use.
228         * <p/>
229         * Notice that both {@link #getDelay()} and {@link #getInitialDelay()} are using
230         * the same time unit. So if you change this value, then take into account that the
231         * default value of {@link #getInitialDelay()} is 1000. So you may to adjust this value accordingly.
232         *
233         * @param timeUnit the time unit.
234         */
235        public void setTimeUnit(TimeUnit timeUnit) {
236            this.timeUnit = timeUnit;
237        }
238    
239        public boolean isUseFixedDelay() {
240            return useFixedDelay;
241        }
242    
243        public void setUseFixedDelay(boolean useFixedDelay) {
244            this.useFixedDelay = useFixedDelay;
245        }
246    
247        public LoggingLevel getRunLoggingLevel() {
248            return runLoggingLevel;
249        }
250    
251        public void setRunLoggingLevel(LoggingLevel runLoggingLevel) {
252            this.runLoggingLevel = runLoggingLevel;
253        }
254    
255        public PollingConsumerPollStrategy getPollStrategy() {
256            return pollStrategy;
257        }
258    
259        public void setPollStrategy(PollingConsumerPollStrategy pollStrategy) {
260            this.pollStrategy = pollStrategy;
261        }
262    
263        public boolean isStartScheduler() {
264            return startScheduler;
265        }
266    
267        /**
268         * Sets whether the scheduler should be started when this consumer starts.
269         * <p/>
270         * This option is default true.
271         *
272         * @param startScheduler whether to start scheduler
273         */
274        public void setStartScheduler(boolean startScheduler) {
275            this.startScheduler = startScheduler;
276        }
277        
278        public void setSendEmptyMessageWhenIdle(boolean sendEmptyMessageWhenIdle) {
279            this.sendEmptyMessageWhenIdle = sendEmptyMessageWhenIdle;
280        }
281        
282        public boolean isSendEmptyMessageWhenIdle() {
283            return sendEmptyMessageWhenIdle;
284        }
285    
286        public ScheduledExecutorService getScheduledExecutorService() {
287            return scheduledExecutorService;
288        }
289    
290        /**
291         * Sets a custom shared {@link ScheduledExecutorService} to use as thread pool
292         * <p/>
293         * <b>Notice: </b> When using a custom thread pool, then the lifecycle of this thread
294         * pool is not controlled by this consumer (eg this consumer will not start/stop the thread pool
295         * when the consumer is started/stopped etc.)
296         *
297         * @param scheduledExecutorService the custom thread pool to use
298         */
299        public void setScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) {
300            this.scheduledExecutorService = scheduledExecutorService;
301        }
302    
303        // Implementation methods
304        // -------------------------------------------------------------------------
305    
306        /**
307         * The polling method which is invoked periodically to poll this consumer
308         *
309         * @return number of messages polled, will be <tt>0</tt> if no message was polled at all.
310         * @throws Exception can be thrown if an exception occurred during polling
311         */
312        protected abstract int poll() throws Exception;
313    
314        @Override
315        protected void doStart() throws Exception {
316            super.doStart();
317    
318            // if no existing executor provided, then create a new thread pool ourselves
319            if (scheduledExecutorService == null) {
320                // we only need one thread in the pool to schedule this task
321                this.scheduledExecutorService = getEndpoint().getCamelContext().getExecutorServiceManager()
322                        .newScheduledThreadPool(this, getEndpoint().getEndpointUri(), 1);
323                // and we should shutdown the thread pool when no longer needed
324                this.shutdownExecutor = true;
325            }
326    
327            ObjectHelper.notNull(scheduledExecutorService, "scheduledExecutorService", this);
328            ObjectHelper.notNull(pollStrategy, "pollStrategy", this);
329    
330            if (isStartScheduler()) {
331                startScheduler();
332            }
333        }
334    
335        protected void startScheduler() {
336            if (isUseFixedDelay()) {
337                if (LOG.isDebugEnabled()) {
338                    LOG.debug("Scheduling poll (fixed delay) with initialDelay: {}, delay: {} ({}) for: {}",
339                            new Object[]{getInitialDelay(), getDelay(), getTimeUnit().name().toLowerCase(Locale.ENGLISH), getEndpoint()});
340                }
341                future = scheduledExecutorService.scheduleWithFixedDelay(this, getInitialDelay(), getDelay(), getTimeUnit());
342            } else {
343                if (LOG.isDebugEnabled()) {
344                    LOG.debug("Scheduling poll (fixed rate) with initialDelay: {}, delay: {} ({}) for: {}",
345                            new Object[]{getInitialDelay(), getDelay(), getTimeUnit().name().toLowerCase(Locale.ENGLISH), getEndpoint()});
346                }
347                future = scheduledExecutorService.scheduleAtFixedRate(this, getInitialDelay(), getDelay(), getTimeUnit());
348            }
349        }
350    
351        @Override
352        protected void doStop() throws Exception {
353            if (future != null) {
354                LOG.debug("This consumer is stopping, so cancelling scheduled task: " + future);
355                future.cancel(false);
356            }
357            super.doStop();
358        }
359    
360        @Override
361        protected void doShutdown() throws Exception {
362            if (shutdownExecutor && scheduledExecutorService != null) {
363                getEndpoint().getCamelContext().getExecutorServiceManager().shutdownNow(scheduledExecutorService);
364                scheduledExecutorService = null;
365                future = null;
366            }
367            super.doShutdown();
368        }
369    
370        @Override
371        protected void doSuspend() throws Exception {
372            // dont stop/cancel the future task since we just check in the run method
373        }
374    
375        @Override
376        public void onInit() throws Exception {
377            // noop
378        }
379    
380        @Override
381        public long beforePoll(long timeout) throws Exception {
382            LOG.trace("Before poll {}", getEndpoint());
383            // resume or start our self
384            if (!ServiceHelper.resumeService(this)) {
385                ServiceHelper.startService(this);
386            }
387    
388            // ensure at least timeout is as long as one poll delay
389            return Math.max(timeout, getDelay());
390        }
391    
392        @Override
393        public void afterPoll() throws Exception {
394            LOG.trace("After poll {}", getEndpoint());
395            // suspend or stop our self
396            if (!ServiceHelper.suspendService(this)) {
397                ServiceHelper.stopService(this);
398            }
399        }
400    
401    }