public synchronized void completedExchange(Exchange exchange, long time) {
    increment();
    exchangesCompleted.increment();
    exchangesInflight.decrement();

    if (ExchangeHelper.isFailureHandled(exchange)) {
      failuresHandled.increment();
    }
    Boolean externalRedelivered = exchange.isExternalRedelivered();
    if (externalRedelivered != null && externalRedelivered) {
      externalRedeliveries.increment();
    }

    minProcessingTime.updateValue(time);
    maxProcessingTime.updateValue(time);
    totalProcessingTime.updateValue(time);
    lastProcessingTime.updateValue(time);
    deltaProcessingTime.updateValue(time);

    long now = new Date().getTime();
    if (firstExchangeCompletedTimestamp.getUpdateCount() == 0) {
      firstExchangeCompletedTimestamp.updateValue(now);
    }

    lastExchangeCompletedTimestamp.updateValue(now);
    if (firstExchangeCompletedExchangeId == null) {
      firstExchangeCompletedExchangeId = exchange.getExchangeId();
    }
    lastExchangeCompletedExchangeId = exchange.getExchangeId();

    // update mean
    long count = exchangesCompleted.getValue();
    long mean = count > 0 ? totalProcessingTime.getValue() / count : 0;
    meanProcessingTime.updateValue(mean);
  }
  /** Strategy to determine if the exchange is done so we can continue */
  protected boolean isDone(Exchange exchange) {
    boolean answer = isCancelledOrInterrupted(exchange);

    // only done if the exchange hasn't failed
    // and it has not been handled by the failure processor
    // or we are exhausted
    if (!answer) {
      answer =
          exchange.getException() == null
              || ExchangeHelper.isFailureHandled(exchange)
              || ExchangeHelper.isRedeliveryExhausted(exchange);
    }

    log.trace("Is exchangeId: {} done? {}", exchange.getExchangeId(), answer);
    return answer;
  }
  protected void handleException(Exchange exchange, RedeliveryData data) {
    Exception e = exchange.getException();

    // store the original caused exception in a property, so we can restore it later
    exchange.setProperty(Exchange.EXCEPTION_CAUGHT, e);

    // find the error handler to use (if any)
    OnExceptionDefinition exceptionPolicy = getExceptionPolicy(exchange, e);
    if (exceptionPolicy != null) {
      data.currentRedeliveryPolicy =
          exceptionPolicy.createRedeliveryPolicy(
              exchange.getContext(), data.currentRedeliveryPolicy);
      data.handledPredicate = exceptionPolicy.getHandledPolicy();
      data.continuedPredicate = exceptionPolicy.getContinuedPolicy();
      data.retryWhilePredicate = exceptionPolicy.getRetryWhilePolicy();
      data.useOriginalInMessage = exceptionPolicy.isUseOriginalMessage();
      data.asyncDelayedRedelivery = exceptionPolicy.isAsyncDelayedRedelivery(exchange.getContext());

      // route specific failure handler?
      Processor processor = exceptionPolicy.getErrorHandler();
      if (processor != null) {
        data.failureProcessor = processor;
      }
      // route specific on redelivery?
      processor = exceptionPolicy.getOnRedelivery();
      if (processor != null) {
        data.onRedeliveryProcessor = processor;
      }
    }

    // only log if not failure handled or not an exhausted unit of work
    if (!ExchangeHelper.isFailureHandled(exchange)
        && !ExchangeHelper.isUnitOfWorkExhausted(exchange)) {
      String msg =
          "Failed delivery for exchangeId: "
              + exchange.getExchangeId()
              + ". On delivery attempt: "
              + data.redeliveryCounter
              + " caught: "
              + e;
      logFailedDelivery(true, false, false, exchange, msg, data, e);
    }

    data.redeliveryCounter = incrementRedeliveryCounter(exchange, e, data);
  }