private void doProcessParallel(final ProcessorExchangePair pair) throws Exception {
    final Exchange exchange = pair.getExchange();
    Processor processor = pair.getProcessor();
    Producer producer = pair.getProducer();

    TracedRouteNodes traced =
        exchange.getUnitOfWork() != null ? exchange.getUnitOfWork().getTracedRouteNodes() : null;

    // compute time taken if sending to another endpoint
    StopWatch watch = null;
    if (producer != null) {
      watch = new StopWatch();
    }

    try {
      // prepare tracing starting from a new block
      if (traced != null) {
        traced.pushBlock();
      }

      if (producer != null) {
        EventHelper.notifyExchangeSending(exchange.getContext(), exchange, producer.getEndpoint());
      }
      // let the prepared process it, remember to begin the exchange pair
      AsyncProcessor async = AsyncProcessorConverterHelper.convert(processor);
      pair.begin();
      // we invoke it synchronously as parallel async routing is too hard
      AsyncProcessorHelper.process(async, exchange);
    } finally {
      pair.done();
      // pop the block so by next round we have the same staring point and thus the tracing looks
      // accurate
      if (traced != null) {
        traced.popBlock();
      }
      if (producer != null) {
        long timeTaken = watch.stop();
        Endpoint endpoint = producer.getEndpoint();
        // emit event that the exchange was sent to the endpoint
        // this is okay to do here in the finally block, as the processing is not using the async
        // routing engine
        // ( we invoke it synchronously as parallel async routing is too hard)
        EventHelper.notifyExchangeSent(exchange.getContext(), exchange, endpoint, timeTaken);
      }
    }
  }
  private boolean doProcessSequential(
      final Exchange original,
      final AtomicExchange result,
      final Iterable<ProcessorExchangePair> pairs,
      final Iterator<ProcessorExchangePair> it,
      final ProcessorExchangePair pair,
      final AsyncCallback callback,
      final AtomicInteger total) {
    boolean sync = true;

    final Exchange exchange = pair.getExchange();
    Processor processor = pair.getProcessor();
    final Producer producer = pair.getProducer();

    TracedRouteNodes traced =
        exchange.getUnitOfWork() != null ? exchange.getUnitOfWork().getTracedRouteNodes() : null;

    // compute time taken if sending to another endpoint
    final StopWatch watch = producer != null ? new StopWatch() : null;

    try {
      // prepare tracing starting from a new block
      if (traced != null) {
        traced.pushBlock();
      }

      if (producer != null) {
        EventHelper.notifyExchangeSending(exchange.getContext(), exchange, producer.getEndpoint());
      }
      // let the prepared process it, remember to begin the exchange pair
      AsyncProcessor async = AsyncProcessorConverterHelper.convert(processor);
      pair.begin();
      sync =
          async.process(
              exchange,
              new AsyncCallback() {
                public void done(boolean doneSync) {
                  // we are done with the exchange pair
                  pair.done();

                  // okay we are done, so notify the exchange was sent
                  if (producer != null) {
                    long timeTaken = watch.stop();
                    Endpoint endpoint = producer.getEndpoint();
                    // emit event that the exchange was sent to the endpoint
                    EventHelper.notifyExchangeSent(
                        exchange.getContext(), exchange, endpoint, timeTaken);
                  }

                  // we only have to handle async completion of the routing slip
                  if (doneSync) {
                    return;
                  }

                  // continue processing the multicast asynchronously
                  Exchange subExchange = exchange;

                  // Decide whether to continue with the multicast or not; similar logic to the
                  // Pipeline
                  // remember to test for stop on exception and aggregate before copying back
                  // results
                  boolean continueProcessing =
                      PipelineHelper.continueProcessing(
                          subExchange,
                          "Sequential processing failed for number " + total.get(),
                          LOG);
                  if (stopOnException && !continueProcessing) {
                    if (subExchange.getException() != null) {
                      // wrap in exception to explain where it failed
                      subExchange.setException(
                          new CamelExchangeException(
                              "Sequential processing failed for number " + total,
                              subExchange,
                              subExchange.getException()));
                    } else {
                      // we want to stop on exception, and the exception was handled by the error
                      // handler
                      // this is similar to what the pipeline does, so we should do the same to not
                      // surprise end users
                      // so we should set the failed exchange as the result and be done
                      result.set(subExchange);
                    }
                    // and do the done work
                    doDone(original, subExchange, pairs, callback, false, true);
                    return;
                  }

                  try {
                    doAggregate(getAggregationStrategy(subExchange), result, subExchange);
                  } catch (Throwable e) {
                    // wrap in exception to explain where it failed
                    subExchange.setException(
                        new CamelExchangeException(
                            "Sequential processing failed for number " + total, subExchange, e));
                    // and do the done work
                    doDone(original, subExchange, pairs, callback, false, true);
                    return;
                  }

                  total.incrementAndGet();

                  // maybe there are more processors to multicast
                  while (it.hasNext()) {

                    // prepare and run the next
                    ProcessorExchangePair pair = it.next();
                    subExchange = pair.getExchange();
                    updateNewExchange(subExchange, total.get(), pairs, it);
                    boolean sync =
                        doProcessSequential(original, result, pairs, it, pair, callback, total);

                    if (!sync) {
                      LOG.trace(
                          "Processing exchangeId: {} is continued being processed asynchronously",
                          original.getExchangeId());
                      return;
                    }

                    // Decide whether to continue with the multicast or not; similar logic to the
                    // Pipeline
                    // remember to test for stop on exception and aggregate before copying back
                    // results
                    continueProcessing =
                        PipelineHelper.continueProcessing(
                            subExchange,
                            "Sequential processing failed for number " + total.get(),
                            LOG);
                    if (stopOnException && !continueProcessing) {
                      if (subExchange.getException() != null) {
                        // wrap in exception to explain where it failed
                        subExchange.setException(
                            new CamelExchangeException(
                                "Sequential processing failed for number " + total,
                                subExchange,
                                subExchange.getException()));
                      } else {
                        // we want to stop on exception, and the exception was handled by the error
                        // handler
                        // this is similar to what the pipeline does, so we should do the same to
                        // not surprise end users
                        // so we should set the failed exchange as the result and be done
                        result.set(subExchange);
                      }
                      // and do the done work
                      doDone(original, subExchange, pairs, callback, false, true);
                      return;
                    }

                    // must catch any exceptions from aggregation
                    try {
                      doAggregate(getAggregationStrategy(subExchange), result, subExchange);
                    } catch (Throwable e) {
                      // wrap in exception to explain where it failed
                      subExchange.setException(
                          new CamelExchangeException(
                              "Sequential processing failed for number " + total, subExchange, e));
                      // and do the done work
                      doDone(original, subExchange, pairs, callback, false, true);
                      return;
                    }

                    total.incrementAndGet();
                  }

                  // do the done work
                  subExchange = result.get() != null ? result.get() : null;
                  doDone(original, subExchange, pairs, callback, false, true);
                }
              });
    } finally {
      // pop the block so by next round we have the same staring point and thus the tracing looks
      // accurate
      if (traced != null) {
        traced.popBlock();
      }
    }

    return sync;
  }