/**
   * This logic is only executed if we have to retry redelivery asynchronously, which have to be
   * done from the callback.
   *
   * <p>And therefore the logic is a bit different than the synchronous <tt>processErrorHandler</tt>
   * method which can use a loop based redelivery technique. However this means that these two
   * methods in overall have to be in <b>sync</b> in terms of logic.
   */
  protected void processAsyncErrorHandler(
      final Exchange exchange, final AsyncCallback callback, final RedeliveryData data) {
    // can we still run
    if (!isRunAllowed()) {
      if (exchange.getException() == null) {
        exchange.setException(new RejectedExecutionException());
      }
      callback.done(data.sync);
      return;
    }

    // did previous processing cause an exception?
    boolean handle = shouldHandleException(exchange);
    if (handle) {
      handleException(exchange, data);
    }

    // compute if we are exhausted or not
    boolean exhausted = isExhausted(exchange, data);
    if (exhausted) {
      Processor target = null;
      boolean deliver = true;

      // the unit of work may have an optional callback associated we need to leverage
      SubUnitOfWorkCallback uowCallback = exchange.getUnitOfWork().getSubUnitOfWorkCallback();
      if (uowCallback != null) {
        // signal to the callback we are exhausted
        uowCallback.onExhausted(exchange);
        // do not deliver to the failure processor as its been handled by the callback instead
        deliver = false;
      }

      if (deliver) {
        // should deliver to failure processor (either from onException or the dead letter channel)
        target = data.failureProcessor != null ? data.failureProcessor : data.deadLetterProcessor;
      }
      // we should always invoke the deliverToFailureProcessor as it prepares, logs and does a fair
      // bit of work for exhausted exchanges (its only the target processor which may be null if
      // handled by a savepoint)
      deliverToFailureProcessor(target, exchange, data, callback);
      // we are breaking out
      return;
    }

    if (data.redeliveryCounter > 0) {
      // let the RedeliverTask be the logic which tries to redeliver the Exchange which we can used
      // a scheduler to
      // have it being executed in the future, or immediately
      // Note: the data.redeliverFromSync should be kept as is, in case it was enabled previously
      // to ensure the callback will continue routing from where we left
      AsyncRedeliveryTask task = new AsyncRedeliveryTask(exchange, callback, data);

      // calculate the redelivery delay
      data.redeliveryDelay =
          data.currentRedeliveryPolicy.calculateRedeliveryDelay(
              data.redeliveryDelay, data.redeliveryCounter);
      if (data.redeliveryDelay > 0) {
        // schedule the redelivery task
        if (log.isTraceEnabled()) {
          log.trace(
              "Scheduling redelivery task to run in {} millis for exchangeId: {}",
              data.redeliveryDelay,
              exchange.getExchangeId());
        }
        executorService.schedule(task, data.redeliveryDelay, TimeUnit.MILLISECONDS);
      } else {
        // execute the task immediately
        executorService.submit(task);
      }
    }
  }
  /** Process the exchange using redelivery error handling. */
  protected boolean processErrorHandler(
      final Exchange exchange, final AsyncCallback callback, final RedeliveryData data) {

    // do a defensive copy of the original Exchange, which is needed for redelivery so we can ensure
    // the
    // original Exchange is being redelivered, and not a mutated Exchange
    data.original = defensiveCopyExchangeIfNeeded(exchange);

    // use looping to have redelivery attempts
    while (true) {

      // can we still run
      if (!isRunAllowed()) {
        if (exchange.getException() == null) {
          exchange.setException(new RejectedExecutionException());
        }
        // we cannot process so invoke callback
        callback.done(data.sync);
        return data.sync;
      }

      // did previous processing cause an exception?
      boolean handle = shouldHandleException(exchange);
      if (handle) {
        handleException(exchange, data);
      }

      // compute if we are exhausted or not
      boolean exhausted = isExhausted(exchange, data);
      if (exhausted) {
        Processor target = null;
        boolean deliver = true;

        // the unit of work may have an optional callback associated we need to leverage
        SubUnitOfWorkCallback uowCallback = exchange.getUnitOfWork().getSubUnitOfWorkCallback();
        if (uowCallback != null) {
          // signal to the callback we are exhausted
          uowCallback.onExhausted(exchange);
          // do not deliver to the failure processor as its been handled by the callback instead
          deliver = false;
        }

        if (deliver) {
          // should deliver to failure processor (either from onException or the dead letter
          // channel)
          target = data.failureProcessor != null ? data.failureProcessor : data.deadLetterProcessor;
        }
        // we should always invoke the deliverToFailureProcessor as it prepares, logs and does a
        // fair
        // bit of work for exhausted exchanges (its only the target processor which may be null if
        // handled by a savepoint)
        boolean sync = deliverToFailureProcessor(target, exchange, data, callback);
        // we are breaking out
        return sync;
      }

      if (data.redeliveryCounter > 0) {
        // calculate delay
        data.redeliveryDelay =
            data.currentRedeliveryPolicy.calculateRedeliveryDelay(
                data.redeliveryDelay, data.redeliveryCounter);

        if (data.redeliveryDelay > 0) {
          // okay there is a delay so create a scheduled task to have it executed in the future

          if (data.currentRedeliveryPolicy.isAsyncDelayedRedelivery() && !exchange.isTransacted()) {
            // let the RedeliverTask be the logic which tries to redeliver the Exchange which we can
            // used a scheduler to
            // have it being executed in the future, or immediately
            // we are continuing asynchronously

            // mark we are routing async from now and that this redelivery task came from a
            // synchronous routing
            data.sync = false;
            data.redeliverFromSync = true;
            AsyncRedeliveryTask task = new AsyncRedeliveryTask(exchange, callback, data);

            // schedule the redelivery task
            if (log.isTraceEnabled()) {
              log.trace(
                  "Scheduling redelivery task to run in {} millis for exchangeId: {}",
                  data.redeliveryDelay,
                  exchange.getExchangeId());
            }
            executorService.schedule(task, data.redeliveryDelay, TimeUnit.MILLISECONDS);

            return false;
          } else {
            // async delayed redelivery was disabled or we are transacted so we must be synchronous
            // as the transaction manager requires to execute in the same thread context
            try {
              data.currentRedeliveryPolicy.sleep(data.redeliveryDelay);
            } catch (InterruptedException e) {
              // we was interrupted so break out
              exchange.setException(e);
              // mark the exchange to stop continue routing when interrupted
              // as we do not want to continue routing (for example a task has been cancelled)
              exchange.setProperty(Exchange.ROUTE_STOP, Boolean.TRUE);
              callback.done(data.sync);
              return data.sync;
            }
          }
        }

        // prepare for redelivery
        prepareExchangeForRedelivery(exchange, data);

        // letting onRedeliver be executed
        deliverToOnRedeliveryProcessor(exchange, data);

        // emmit event we are doing redelivery
        EventHelper.notifyExchangeRedelivery(
            exchange.getContext(), exchange, data.redeliveryCounter);
      }

      // process the exchange (also redelivery)
      boolean sync =
          AsyncProcessorHelper.process(
              outputAsync,
              exchange,
              new AsyncCallback() {
                public void done(boolean sync) {
                  // this callback should only handle the async case
                  if (sync) {
                    return;
                  }

                  // mark we are in async mode now
                  data.sync = false;

                  // if we are done then notify callback and exit
                  if (isDone(exchange)) {
                    callback.done(sync);
                    return;
                  }

                  // error occurred so loop back around which we do by invoking the
                  // processAsyncErrorHandler
                  // method which takes care of this in a asynchronous manner
                  processAsyncErrorHandler(exchange, callback, data);
                }
              });

      if (!sync) {
        // the remainder of the Exchange is being processed asynchronously so we should return
        return false;
      }
      // we continue to route synchronously

      // if we are done then notify callback and exit
      boolean done = isDone(exchange);
      if (done) {
        callback.done(true);
        return true;
      }

      // error occurred so loop back around.....
    }
  }