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 }