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.processor;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Iterator;
022import java.util.LinkedList;
023import java.util.List;
024import java.util.Queue;
025import java.util.concurrent.ConcurrentLinkedQueue;
026import java.util.concurrent.TimeUnit;
027import java.util.concurrent.locks.Condition;
028import java.util.concurrent.locks.Lock;
029import java.util.concurrent.locks.ReentrantLock;
030
031import org.apache.camel.AsyncCallback;
032import org.apache.camel.AsyncProcessor;
033import org.apache.camel.CamelContext;
034import org.apache.camel.CamelExchangeException;
035import org.apache.camel.Exchange;
036import org.apache.camel.Expression;
037import org.apache.camel.Navigate;
038import org.apache.camel.Predicate;
039import org.apache.camel.Processor;
040import org.apache.camel.spi.ExceptionHandler;
041import org.apache.camel.spi.IdAware;
042import org.apache.camel.support.LoggingExceptionHandler;
043import org.apache.camel.support.ServiceSupport;
044import org.apache.camel.util.AsyncProcessorHelper;
045import org.apache.camel.util.ObjectHelper;
046import org.apache.camel.util.ServiceHelper;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050/**
051 * A base class for any kind of {@link Processor} which implements some kind of batch processing.
052 * 
053 * @version 
054 * @deprecated may be removed in the future when we overhaul the resequencer EIP
055 */
056@Deprecated
057public class BatchProcessor extends ServiceSupport implements AsyncProcessor, Navigate<Processor>, IdAware {
058
059    public static final long DEFAULT_BATCH_TIMEOUT = 1000L;
060    public static final int DEFAULT_BATCH_SIZE = 100;
061
062    private static final Logger LOG = LoggerFactory.getLogger(BatchProcessor.class);
063
064    private String id;
065    private long batchTimeout = DEFAULT_BATCH_TIMEOUT;
066    private int batchSize = DEFAULT_BATCH_SIZE;
067    private int outBatchSize;
068    private boolean groupExchanges;
069    private boolean batchConsumer;
070    private boolean ignoreInvalidExchanges;
071    private boolean reverse;
072    private boolean allowDuplicates;
073    private Predicate completionPredicate;
074    private Expression expression;
075
076    private final CamelContext camelContext;
077    private final Processor processor;
078    private final Collection<Exchange> collection;
079    private ExceptionHandler exceptionHandler;
080
081    private final BatchSender sender;
082
083    public BatchProcessor(CamelContext camelContext, Processor processor, Collection<Exchange> collection, Expression expression) {
084        ObjectHelper.notNull(camelContext, "camelContext");
085        ObjectHelper.notNull(processor, "processor");
086        ObjectHelper.notNull(collection, "collection");
087        ObjectHelper.notNull(expression, "expression");
088
089        // wrap processor in UnitOfWork so what we send out of the batch runs in a UoW
090        this.camelContext = camelContext;
091        this.processor = processor;
092        this.collection = collection;
093        this.expression = expression;
094        this.sender = new BatchSender();
095        this.exceptionHandler = new LoggingExceptionHandler(camelContext, getClass());
096    }
097
098    @Override
099    public String toString() {
100        return "BatchProcessor[to: " + processor + "]";
101    }
102
103    // Properties
104    // -------------------------------------------------------------------------
105
106
107    public Expression getExpression() {
108        return expression;
109    }
110
111    public ExceptionHandler getExceptionHandler() {
112        return exceptionHandler;
113    }
114
115    public void setExceptionHandler(ExceptionHandler exceptionHandler) {
116        this.exceptionHandler = exceptionHandler;
117    }
118
119    public int getBatchSize() {
120        return batchSize;
121    }
122
123    /**
124     * Sets the <b>in</b> batch size. This is the number of incoming exchanges that this batch processor will
125     * process before its completed. The default value is {@link #DEFAULT_BATCH_SIZE}.
126     * 
127     * @param batchSize the size
128     */
129    public void setBatchSize(int batchSize) {
130        // setting batch size to 0 or negative is like disabling it, so we set it as the max value
131        // as the code logic is dependent on a batch size having 1..n value
132        if (batchSize <= 0) {
133            LOG.debug("Disabling batch size, will only be triggered by timeout");
134            this.batchSize = Integer.MAX_VALUE;
135        } else {
136            this.batchSize = batchSize;
137        }
138    }
139
140    public int getOutBatchSize() {
141        return outBatchSize;
142    }
143
144    /**
145     * Sets the <b>out</b> batch size. If the batch processor holds more exchanges than this out size then the
146     * completion is triggered. Can for instance be used to ensure that this batch is completed when a certain
147     * number of exchanges has been collected. By default this feature is <b>not</b> enabled.
148     * 
149     * @param outBatchSize the size
150     */
151    public void setOutBatchSize(int outBatchSize) {
152        this.outBatchSize = outBatchSize;
153    }
154
155    public long getBatchTimeout() {
156        return batchTimeout;
157    }
158
159    public void setBatchTimeout(long batchTimeout) {
160        this.batchTimeout = batchTimeout;
161    }
162
163    public boolean isGroupExchanges() {
164        return groupExchanges;
165    }
166
167    public void setGroupExchanges(boolean groupExchanges) {
168        this.groupExchanges = groupExchanges;
169    }
170
171    public boolean isBatchConsumer() {
172        return batchConsumer;
173    }
174
175    public void setBatchConsumer(boolean batchConsumer) {
176        this.batchConsumer = batchConsumer;
177    }
178
179    public boolean isIgnoreInvalidExchanges() {
180        return ignoreInvalidExchanges;
181    }
182
183    public void setIgnoreInvalidExchanges(boolean ignoreInvalidExchanges) {
184        this.ignoreInvalidExchanges = ignoreInvalidExchanges;
185    }
186
187    public boolean isReverse() {
188        return reverse;
189    }
190
191    public void setReverse(boolean reverse) {
192        this.reverse = reverse;
193    }
194
195    public boolean isAllowDuplicates() {
196        return allowDuplicates;
197    }
198
199    public void setAllowDuplicates(boolean allowDuplicates) {
200        this.allowDuplicates = allowDuplicates;
201    }
202
203    public Predicate getCompletionPredicate() {
204        return completionPredicate;
205    }
206
207    public void setCompletionPredicate(Predicate completionPredicate) {
208        this.completionPredicate = completionPredicate;
209    }
210
211    public Processor getProcessor() {
212        return processor;
213    }
214
215    public List<Processor> next() {
216        if (!hasNext()) {
217            return null;
218        }
219        List<Processor> answer = new ArrayList<Processor>(1);
220        answer.add(processor);
221        return answer;
222    }
223
224    public boolean hasNext() {
225        return processor != null;
226    }
227
228    public String getId() {
229        return id;
230    }
231
232    public void setId(String id) {
233        this.id = id;
234    }
235
236    /**
237     * A strategy method to decide if the "in" batch is completed. That is, whether the resulting exchanges in
238     * the in queue should be drained to the "out" collection.
239     */
240    private boolean isInBatchCompleted(int num) {
241        return num >= batchSize;
242    }
243
244    /**
245     * A strategy method to decide if the "out" batch is completed. That is, whether the resulting exchange in
246     * the out collection should be sent.
247     */
248    private boolean isOutBatchCompleted() {
249        if (outBatchSize == 0) {
250            // out batch is disabled, so go ahead and send.
251            return true;
252        }
253        return collection.size() > 0 && collection.size() >= outBatchSize;
254    }
255
256    /**
257     * Strategy Method to process an exchange in the batch. This method allows derived classes to perform
258     * custom processing before or after an individual exchange is processed
259     */
260    protected void processExchange(Exchange exchange) throws Exception {
261        processor.process(exchange);
262        if (exchange.getException() != null) {
263            getExceptionHandler().handleException("Error processing aggregated exchange: " + exchange, exchange.getException());
264        }
265    }
266
267    protected void doStart() throws Exception {
268        ServiceHelper.startServices(processor);
269        sender.start();
270    }
271
272    protected void doStop() throws Exception {
273        sender.cancel();
274        ServiceHelper.stopServices(processor);
275        collection.clear();
276    }
277
278    public void process(Exchange exchange) throws Exception {
279        AsyncProcessorHelper.process(this, exchange);
280    }
281
282    /**
283     * Enqueues an exchange for later batch processing.
284     */
285    public boolean process(Exchange exchange, AsyncCallback callback) {
286        try {
287            // if batch consumer is enabled then we need to adjust the batch size
288            // with the size from the batch consumer
289            if (isBatchConsumer()) {
290                int size = exchange.getProperty(Exchange.BATCH_SIZE, Integer.class);
291                if (batchSize != size) {
292                    batchSize = size;
293                    LOG.trace("Using batch consumer completion, so setting batch size to: {}", batchSize);
294                }
295            }
296
297            // validate that the exchange can be used
298            if (!isValid(exchange)) {
299                if (isIgnoreInvalidExchanges()) {
300                    LOG.debug("Invalid Exchange. This Exchange will be ignored: {}", exchange);
301                } else {
302                    throw new CamelExchangeException("Exchange is not valid to be used by the BatchProcessor", exchange);
303                }
304            } else {
305                // exchange is valid so enqueue the exchange
306                sender.enqueueExchange(exchange);
307            }
308        } catch (Throwable e) {
309            exchange.setException(e);
310        }
311        callback.done(true);
312        return true;
313    }
314
315    /**
316     * Is the given exchange valid to be used.
317     *
318     * @param exchange the given exchange
319     * @return <tt>true</tt> if valid, <tt>false</tt> otherwise
320     */
321    private boolean isValid(Exchange exchange) {
322        Object result = null;
323        try {
324            result = expression.evaluate(exchange, Object.class);
325        } catch (Exception e) {
326            // ignore
327        }
328        return result != null;
329    }
330
331    /**
332     * Sender thread for queued-up exchanges.
333     */
334    private class BatchSender extends Thread {
335
336        private Queue<Exchange> queue;
337        private Lock queueLock = new ReentrantLock();
338        private boolean exchangeEnqueued;
339        private final Queue<String> completionPredicateMatched = new ConcurrentLinkedQueue<String>();
340        private Condition exchangeEnqueuedCondition = queueLock.newCondition();
341
342        public BatchSender() {
343            super(camelContext.getExecutorServiceManager().resolveThreadName("Batch Sender"));
344            this.queue = new LinkedList<Exchange>();
345        }
346
347        @Override
348        public void run() {
349            // Wait until one of either:
350            // * an exchange being queued;
351            // * the batch timeout expiring; or
352            // * the thread being cancelled.
353            //
354            // If an exchange is queued then we need to determine whether the
355            // batch is complete. If it is complete then we send out the batched
356            // exchanges. Otherwise we move back into our wait state.
357            //
358            // If the batch times out then we send out the batched exchanges
359            // collected so far.
360            //
361            // If we receive an interrupt then all blocking operations are
362            // interrupted and our thread terminates.
363            //
364            // The goal of the following algorithm in terms of synchronisation
365            // is to provide fine grained locking i.e. retaining the lock only
366            // when required. Special consideration is given to releasing the
367            // lock when calling an overloaded method i.e. sendExchanges. 
368            // Unlocking is important as the process of sending out the exchanges
369            // would otherwise block new exchanges from being queued.
370
371            queueLock.lock();
372            try {
373                do {
374                    try {
375                        if (!exchangeEnqueued) {
376                            LOG.trace("Waiting for new exchange to arrive or batchTimeout to occur after {} ms.", batchTimeout);
377                            exchangeEnqueuedCondition.await(batchTimeout, TimeUnit.MILLISECONDS);
378                        }
379
380                        // if the completion predicate was triggered then there is an exchange id which denotes when to complete
381                        String id = null;
382                        if (!completionPredicateMatched.isEmpty()) {
383                            id = completionPredicateMatched.poll();
384                        }
385
386                        if (id != null || !exchangeEnqueued) {
387                            if (id != null) {
388                                LOG.trace("Collecting exchanges to be aggregated triggered by completion predicate");
389                            } else {
390                                LOG.trace("Collecting exchanges to be aggregated triggered by batch timeout");
391                            }
392                            drainQueueTo(collection, batchSize, id);
393                        } else {
394                            exchangeEnqueued = false;
395                            boolean drained = false;
396                            while (isInBatchCompleted(queue.size())) {
397                                drained = true;
398                                drainQueueTo(collection, batchSize, id);
399                            }
400                            if (drained) {
401                                LOG.trace("Collecting exchanges to be aggregated triggered by new exchanges received");
402                            }
403
404                            if (!isOutBatchCompleted()) {
405                                continue;
406                            }
407                        }
408
409                        queueLock.unlock();
410                        try {
411                            try {
412                                sendExchanges();
413                            } catch (Throwable t) {
414                                // a fail safe to handle all exceptions being thrown
415                                getExceptionHandler().handleException(t);
416                            }
417                        } finally {
418                            queueLock.lock();
419                        }
420
421                    } catch (InterruptedException e) {
422                        break;
423                    }
424
425                } while (isRunAllowed());
426
427            } finally {
428                queueLock.unlock();
429            }
430        }
431
432        /**
433         * This method should be called with queueLock held
434         */
435        private void drainQueueTo(Collection<Exchange> collection, int batchSize, String exchangeId) {
436            for (int i = 0; i < batchSize; ++i) {
437                Exchange e = queue.poll();
438                if (e != null) {
439                    try {
440                        collection.add(e);
441                    } catch (Exception t) {
442                        e.setException(t);
443                    } catch (Throwable t) {
444                        getExceptionHandler().handleException(t);
445                    }
446                    if (exchangeId != null && exchangeId.equals(e.getExchangeId())) {
447                        // this batch is complete so stop draining
448                        break;
449                    }
450                } else {
451                    break;
452                }
453            }
454        }
455
456        public void cancel() {
457            interrupt();
458        }
459
460        public void enqueueExchange(Exchange exchange) {
461            LOG.debug("Received exchange to be batched: {}", exchange);
462            queueLock.lock();
463            try {
464                // pre test whether the completion predicate matched
465                if (completionPredicate != null) {
466                    boolean matches = completionPredicate.matches(exchange);
467                    if (matches) {
468                        LOG.trace("Exchange matched completion predicate: {}", exchange);
469                        // add this exchange to the list of exchanges which marks the batch as complete
470                        completionPredicateMatched.add(exchange.getExchangeId());
471                    }
472                }
473                queue.add(exchange);
474                exchangeEnqueued = true;
475                exchangeEnqueuedCondition.signal();
476            } finally {
477                queueLock.unlock();
478            }
479        }
480        
481        private void sendExchanges() throws Exception {
482            Iterator<Exchange> iter = collection.iterator();
483            while (iter.hasNext()) {
484                Exchange exchange = iter.next();
485                iter.remove();
486                try {
487                    LOG.debug("Sending aggregated exchange: {}", exchange);
488                    processExchange(exchange);
489                } catch (Throwable t) {
490                    // must catch throwable to avoid growing memory
491                    getExceptionHandler().handleException("Error processing aggregated exchange: " + exchange, t);
492                }
493            }
494        }
495    }
496
497}