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}