All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.esbtools.eventhandler.RetryingBatchFailedMessageRoute Maven / Gradle / Ivy

/*
 *  Copyright 2016 esbtools Contributors and/or its affiliates.
 *
 *  This file is part of esbtools.
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see .
 */

package org.esbtools.eventhandler;

import static org.apache.camel.builder.PredicateBuilder.and;
import static org.apache.camel.builder.PredicateBuilder.not;

import org.apache.camel.Exchange;
import org.apache.camel.Expression;
import org.apache.camel.Predicate;
import org.apache.camel.builder.RouteBuilder;

import javax.annotation.Nullable;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

public class RetryingBatchFailedMessageRoute extends RouteBuilder {
    private final String fromUri;
    private final Expression retryDelayMillis;
    private final int maxRetryCount;
    private final Duration processTimeout;
    private final String deadLetterUri;

    private final int idCount = idCounter.getAndIncrement();
    private final String routeId = "failedMessageRetryer-" + idCount;

    private static final AtomicInteger idCounter = new AtomicInteger(0);

    private static final String NEXT_ATTEMPT_NUMBER_PROPERTY = "nextAttemptNumber";
    private static final Integer FIRST_ATTEMPT_NUMBER = 1;

    public RetryingBatchFailedMessageRoute(String fromUri, Expression retryDelayMillis,
            int maxRetryCount, Duration processTimeout, String deadLetterUri) {
        this.fromUri = fromUri;
        this.retryDelayMillis = retryDelayMillis;
        this.maxRetryCount = maxRetryCount;
        this.processTimeout = processTimeout;
        this.deadLetterUri = deadLetterUri;
    }

    @Override
    public void configure() throws Exception {
        from(fromUri)
        .routeId(routeId)
        // We use loop instead of error handler because error handlers start with original message
        // sent to point of failure; we need the message to stay intact to prevent reprocessing
        // already succeeded messages and to keep context of previous tries' failures.
        .loopDoWhile(and(
                exchangeHasFailures(),
                not(maxRetryCountMet())))
            .delay(retryDelayMillis)
                // Indenting because delay actually starts a child processor, and needs its own end()
                // See: https://issues.apache.org/jira/browse/CAMEL-2654
                .process(exchange -> {
                    int retryAttempt = Optional.ofNullable(
                            exchange.getProperty(NEXT_ATTEMPT_NUMBER_PROPERTY, Integer.class))
                            .orElse(FIRST_ATTEMPT_NUMBER);
                    // Preemptively increment retry attempt for next loop.
                    exchange.setProperty(NEXT_ATTEMPT_NUMBER_PROPERTY, retryAttempt + 1);

                    Collection oldFailures = exchange.getIn().getMandatoryBody(Collection.class);

                    List newFailures = new ArrayList<>();
                    List reprocessingFailures =
                            new ArrayList<>(oldFailures.size());

                    log.debug("About to retry {} messages on route {}, attempt #{}: {}",
                            oldFailures.size(), routeId, retryAttempt, oldFailures);

                    // Begin processing all failed messages again in parallel.
                    for (Object failureAsObject : oldFailures) {
                        if (!(failureAsObject instanceof FailedMessage)) {
                            throw new IllegalArgumentException("Messages sent to " +
                                    RetryingBatchFailedMessageRoute.class + " route should be " +
                                    "collections of FailedMessage elements, but got collection " +
                                    "of " + failureAsObject.getClass());
                        }

                        FailedMessage failure = (FailedMessage) failureAsObject;
                        Optional maybeMessage = failure.parsedMessage();

                        if (!maybeMessage.isPresent()) {
                            // Nothing to retry; dead letter it
                            // This happens when message factory failed to get message from original
                            // body. We won't bother trying get the message from the message factory
                            // again; if that fails it is usually a bug that retrying won't circumvent.
                            log.warn("Failed message had no parsed message. There is no message " +
                                    "to retry without trying to parse again, which is usually " +
                                    "fruitless. Sending to dead letter URI {}.", deadLetterUri);
                            newFailures.add(failure);
                            continue;
                        }

                        Message message = maybeMessage.get();
                        final Future reprocessingFuture;

                        try {
                            reprocessingFuture = message.process();
                        } catch (Exception e) {
                            log.error("Failed to reprocess message (retry attempt #" +
                                    retryAttempt + "): " + message, e);
                            suppressPreviousFailureInNewException(failure, e);
                            newFailures.add(new FailedMessage(failure.originalMessage(), message, e));
                            continue;
                        }

                        reprocessingFailures.add(new ReprocessingFailure(failure, reprocessingFuture));
                    }

                    List reprocessedSuccessfully = log.isDebugEnabled()
                            ? new ArrayList<>(reprocessingFailures.size())
                            : Collections.emptyList();

                    for (ReprocessingFailure reprocessingFailure : reprocessingFailures) {
                        FailedMessage originalFailure = reprocessingFailure.originalFailure;

                        try {
                            reprocessingFailure.reprocessingFuture
                                    .get(processTimeout.toMillis(), TimeUnit.MILLISECONDS);

                            if (log.isDebugEnabled()) {
                                reprocessedSuccessfully.add(originalFailure.parsedMessage().get());
                            }
                        } catch (ExecutionException e) {
                            Message parsedMessage = originalFailure.parsedMessage().get();

                            log.error("Failed to reprocess message (retry attempt #" + retryAttempt +
                                    "): " + parsedMessage, e);

                            Throwable realException = e.getCause();
                            suppressPreviousFailureInNewException(originalFailure, realException);

                            FailedMessage failure = new FailedMessage(
                                    originalFailure.originalMessage(), parsedMessage, realException);
                            newFailures.add(failure);
                        } catch (InterruptedException | TimeoutException e) {
                            Message parsedMessage = originalFailure.parsedMessage().get();

                            log.warn("Timed out reprocessing message (retry attempt #" + retryAttempt +
                                    "): " + parsedMessage, e);

                            suppressPreviousFailureInNewException(originalFailure, e);
                            FailedMessage failure = new FailedMessage(
                                    originalFailure.originalMessage(), parsedMessage, e);
                            newFailures.add(failure);
                        }
                    }

                    log.debug("Retry attempt #{} successfully processed {}/{} messages " +
                            "on route {}: {}",
                            retryAttempt, reprocessedSuccessfully.size(), oldFailures.size(),
                            routeId, reprocessedSuccessfully);

                    // Give new failures another shot or dead letter them.
                    exchange.getIn().setBody(newFailures);
                })
            .end() // end delay -- see comment below delay(...).
        .end() // end loop
        // If we still have failures, dead letter them.
        .filter(exchangeHasFailures())
        .to(deadLetterUri);
    }

    /**
     * In the event a messages fails on subsequent retries, this tracks that previous failure as a
     * suppressed exception in the latest failure, keeping the history of failures for debugging.
     *
     * 

Makes sure the exceptions are not referring to the same object to avoid a infinite * recursion. */ private void suppressPreviousFailureInNewException(FailedMessage previousMsg, Throwable _new) { Throwable previous = previousMsg.exception(); if (areExceptionsEqual(_new, previous)) { Arrays.stream(previous.getSuppressed()) .filter(e -> e != _new) .forEach(_new::addSuppressed); } else { _new.addSuppressed(previous); } } private Predicate maxRetryCountMet() { return new Predicate() { @Override public boolean matches(Exchange exchange) { Integer nextAttemptNumber = Optional.ofNullable( exchange.getProperty(NEXT_ATTEMPT_NUMBER_PROPERTY, Integer.class)) .orElse(FIRST_ATTEMPT_NUMBER); return nextAttemptNumber - FIRST_ATTEMPT_NUMBER >= maxRetryCount; } }; } private Predicate exchangeHasFailures() { return new Predicate() { @Override public boolean matches(Exchange exchange) { Collection failures = exchange.getIn().getBody(Collection.class); if (failures == null || failures.isEmpty()) { return false; } return true; } }; } private static boolean areExceptionsEqual(@Nullable Throwable t1, @Nullable Throwable t2) { if (t1 == t2 || Objects.equals(t1, t2)) { return true; } if (t1 == null || t2 == null) { return false; } if (!Objects.equals(t1.getMessage(), t2.getMessage())) { return false; } if (!Objects.equals(t1.getClass(), t2.getClass())) { return false; } if (!Objects.deepEquals(t1.getStackTrace(), t2.getStackTrace())) { return false; } return areExceptionsEqual(t1.getCause(), t2.getCause()); } private static final class ReprocessingFailure { private final FailedMessage originalFailure; private final Future reprocessingFuture; private ReprocessingFailure(FailedMessage originalFailure, Future reprocessingFuture) { this.originalFailure = originalFailure; this.reprocessingFuture = reprocessingFuture; } @Override public String toString() { return "ReprocessingFailure{" + "originalFailure=" + originalFailure + '}'; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy