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

org.kiwiproject.retry.KiwiRetryer Maven / Gradle / Ivy

Go to download

Kiwi is a utility library. We really like Google's Guava, and also use Apache Commons. But if they don't have something we need, and we think it is useful, this is where we put it.

There is a newer version: 4.5.2
Show newest version
package org.kiwiproject.retry;

import static org.kiwiproject.base.KiwiStrings.f;
import static org.kiwiproject.collect.KiwiLists.isNotNullOrEmpty;
import static org.kiwiproject.retry.KiwiRetryerPredicates.CONNECTION_ERROR;
import static org.kiwiproject.retry.KiwiRetryerPredicates.NO_ROUTE_TO_HOST;
import static org.kiwiproject.retry.KiwiRetryerPredicates.SOCKET_TIMEOUT;
import static org.kiwiproject.retry.KiwiRetryerPredicates.UNKNOWN_HOST;

import lombok.Builder;
import lombok.Singular;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.kiwiproject.base.KiwiThrowables;
import org.kiwiproject.base.UUIDs;
import org.slf4j.event.Level;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

/**
 * This is a wrapper class for {@link Retryer}; it wraps methods so that the {@link RetryException} and
 * {@link InterruptedException} that are generated from the {@link Retryer#call(Callable)} method are converted to
 * {@link KiwiRetryerException}. The Background Information section at the bottom gives some history on
 * why this is even here in the first place. You might consider using {@link Retryer} directly now that it is
 * within org.kiwiproject as the standalone
 * retrying-again library.
 * 

* It also provides some kiwi-flavored default values, an identifier that can be used to distinguish between * retryer instances in logs, logging of retry attempts, and some factories for creating retryer instances * with common behavior. *

* You can construct a {@link KiwiRetryer} using the builder obtained via {@code KiwiRetryer.builder()}. *

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Available configuration options for KiwiRetryer:
NameDefaultDescription
retryerIdUUID generated by {@link UUIDs#randomUUIDString()}The identifier for the retryer
initialSleepTimeAmount100 * The initial sleep amount for the default incrementing wait strategy. * This value will be ignored if an explicit {@link WaitStrategy} is defined. *
initialSleepTimeUnit{@link TimeUnit#MILLISECONDS} * The initial sleep {@link TimeUnit} for the default incrementing wait strategy. * This value will be ignored if an explicit {@link WaitStrategy} is defined. *
retryIncrementTimeAmount200 * The subsequent retry increment amount for the default incrementing wait strategy. * This value will be ignored if an explicit {@link WaitStrategy} is defined. *
retryIncrementTimeUnit{@link TimeUnit#MILLISECONDS} * The subsequent retry increment {@link TimeUnit} for the default incrementing wait strategy. * This value will be ignored if an explicit {@link WaitStrategy} is defined. *
maxAttempts5 * The maximum number of attempts to use for the default stop strategy. * This value will be ignored if an explicit {@link StopStrategy} is defined. *
processingLogLevel{@link Level#DEBUG}This log level controls the "happy path" messages that are logged by this retryer.
exceptionLogLevel{@link Level#WARN}This log level controls the "sad path" messages (i.e. exceptions) that are logged by this retryer.
retryOnAllExceptionsfalse * Tells the retryer to retry on any exception. NOTE: This supersedes any exceptions added to the * {@code exceptionPredicates} list or if {@code retryOnAllRuntimeExceptions} is set to {@code true} *
retryOnAllRuntimeExceptionsfalse * Tells the retryer to retry on all {@link RuntimeException}s. NOTE: This supersedes any exceptions * added to the {@code exceptionPredicates} list. *
exceptionPredicatesempty list * Defines the {@link Exception}s that should cause KiwiRetryer to retry its specified {@link Callable} if * encountered during processing. *
resultPredicatesempty list * Defines the {@code T} objects that should cause KiwiRetryer to retry its specified {@link Callable} if * returned by the {@link Callable} during processing. *
stopStrategynullAn explicit {@code stopStrategy} which will override the default stop after attempts stop strategy.
waitStrategynullAn explicit {@code waitStrategy} which will override the default incrementing wait strategy.
*

* Background Information: *

* Originally this class was created to wrap the (now defunct) guava-retrying * library and add various defaults and conveniences, as well as an easier way to handle RetryException and its causes. * The guava-retrying library stopped being maintained circa 2016, and was forked into * re-retrying, which then stopped active development circa 2018. * So, in late 2020 we forked re-retrying as retrying-again in order to keep it up to date at a minimum, and hopefully * add some additional value where it makes sense. It is possible we might simply move some of this functionality * into retrying-again and then deprecate and remove this functionality from kiwi. That would also allow us to use * kiwi from retrying-again, which we currently cannot do without introducing a circular dependency. *

* NOTE: The org.kiwiproject:retrying-again library must be available at runtime. */ @Slf4j @Builder public class KiwiRetryer { private static final long DEFAULT_INITIAL_SLEEP_TIME_MILLISECONDS = 100; private static final long DEFAULT_RETRY_INCREMENT_TIME_MILLISECONDS = 200; private static final int DEFAULT_MAXIMUM_ATTEMPTS = 5; @Builder.Default private final String retryerId = UUIDs.randomUUIDString(); @Builder.Default private final long initialSleepTimeAmount = DEFAULT_INITIAL_SLEEP_TIME_MILLISECONDS; @Builder.Default private final TimeUnit initialSleepTimeUnit = TimeUnit.MILLISECONDS; @Builder.Default private final long retryIncrementTimeAmount = DEFAULT_RETRY_INCREMENT_TIME_MILLISECONDS; @Builder.Default private final TimeUnit retryIncrementTimeUnit = TimeUnit.MILLISECONDS; @Builder.Default private final int maxAttempts = DEFAULT_MAXIMUM_ATTEMPTS; @Builder.Default private final Level processingLogLevel = Level.DEBUG; @Builder.Default private final Level exceptionLogLevel = Level.WARN; private final boolean retryOnAllExceptions; private final boolean retryOnAllRuntimeExceptions; @Singular private final List> exceptionPredicates; @Singular private final List> resultPredicates; private final StopStrategy stopStrategy; private final WaitStrategy waitStrategy; /** * Create a new instance with only the default values. * * @param type of object the retryer returns * @return a KiwiRetryer for type {@code T} */ public static KiwiRetryer newRetryerWithDefaults() { return KiwiRetryer.builder().build(); } /** * Create a new instance with several common network-related exception predicates. * * @param retryerId the retryer ID * @param type of object the retryer returns * @return a KiwiRetryer for type {@code T} * @see KiwiRetryerPredicates#CONNECTION_ERROR * @see KiwiRetryerPredicates#NO_ROUTE_TO_HOST * @see KiwiRetryerPredicates#SOCKET_TIMEOUT * @see KiwiRetryerPredicates#UNKNOWN_HOST */ public static KiwiRetryer newRetryerWithDefaultExceptions(String retryerId) { return KiwiRetryer.builder() .retryerId(retryerId) .exceptionPredicate(CONNECTION_ERROR) .exceptionPredicate(NO_ROUTE_TO_HOST) .exceptionPredicate(SOCKET_TIMEOUT) .exceptionPredicate(UNKNOWN_HOST) .build(); } /** * Create a new instance that will retry on all exceptions. * * @param retryerId the retryer ID * @param type of object the retryer returns * @return a KiwiRetryer for type {@code T} */ public static KiwiRetryer newRetryerRetryingAllExceptions(String retryerId) { return KiwiRetryer.builder() .retryerId(retryerId) .retryOnAllExceptions(true) .build(); } /** * Create a new instance that will retry on all runtime exceptions. * * @param retryerId the retryer ID * @param type of object the retryer returns * @return a KiwiRetryer for type {@code T} */ public static KiwiRetryer newRetryerRetryingAllRuntimeExceptions(String retryerId) { return KiwiRetryer.builder() .retryerId(retryerId) .retryOnAllRuntimeExceptions(true) .build(); } /** * Invoke the retryer with the given {@link Callable}. * * @param callable the code that attempts to produce a result * @return the result of the {@link Callable} * @throws KiwiRetryerException if there was an unhandled exception during processing, or if the maximum * number of attempts was reached without success. For further information about the cause, you can * unwrap the exception. * @see KiwiRetryerException#unwrapKiwiRetryerException(KiwiRetryerException) * @see KiwiRetryerException#unwrapKiwiRetryerExceptionFully(KiwiRetryerException) */ public T call(Callable callable) { return call(retryerId, callable); } /** * Invoke the retryer with the given ID and {@link Callable}. *

* This method allows you to use different IDs with the same {@link KiwiRetryer} instance, for example if * the same retryer is called in separate threads it will be useful to be able to distinguish between them * in logs. * * @param retryerId the ID for this retryer call (overrides the {@code retryerId} of this instance) * @param callable the code that attempts to produce a result * @return the result of the {@link Callable} * @throws KiwiRetryerException if there was an unhandled exception during processing, or if the maximum * number of attempts was reached without success. For further information about * the cause, you can unwrap the exception. * @see KiwiRetryerException#unwrapKiwiRetryerException(KiwiRetryerException) * @see KiwiRetryerException#unwrapKiwiRetryerExceptionFully(KiwiRetryerException) */ public T call(String retryerId, Callable callable) { try { var retryer = buildRetryer(retryerId); LOG.debug("Calling retryer with id: {}", retryerId); return retryer.call(callable); } catch (RetryException e) { var message = f("KiwiRetryer {} failed all {} attempts. Error: {}", retryerId, e.getNumberOfFailedAttempts(), e.getMessage()); throw new KiwiRetryerException(message, e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); var message = f("KiwiRetryer {} interrupted making call. Wrapped exception: {}", retryerId, e.getCause()); throw new KiwiRetryerException(message, e); } } private Retryer buildRetryer(String retryerId) { var theWaitStrategy = determineWaitStrategy(); var theStopStrategy = determineStopStrategy(); var theLogListener = new LoggingRetryListener(retryerId, processingLogLevel, exceptionLogLevel); var retryerBuilder = RetryerBuilder.newBuilder() .withWaitStrategy(theWaitStrategy) .withStopStrategy(theStopStrategy) .withRetryListener(theLogListener); if (retryOnAllExceptions) { logIfRetryOnRuntimeExceptionsIsSet(); logIfExceptionPredicatesIsNotEmpty(); retryerBuilder.retryIfException(); } else if (retryOnAllRuntimeExceptions) { logIfExceptionPredicatesIsNotEmpty(); retryerBuilder.retryIfRuntimeException(); } else { exceptionPredicates.forEach(retryerBuilder::retryIfException); } resultPredicates.forEach(retryerBuilder::retryIfResult); return retryerBuilder.build(); } private void logIfRetryOnRuntimeExceptionsIsSet() { if (retryOnAllRuntimeExceptions) { LOG.warn("Both retryOnAllExceptions and retryOnAllRuntimeExceptions are set;" + " retryOnAllExceptions takes precedence"); } } private void logIfExceptionPredicatesIsNotEmpty() { if (isNotNullOrEmpty(exceptionPredicates)) { var field = retryOnAllExceptions ? "retryOnAllExceptions" : "retryOnAllRuntimeExceptions"; LOG.warn("{} is set while exceptionPredicates is populated: {} takes precedence", field, field); } } private WaitStrategy determineWaitStrategy() { return Optional.ofNullable(waitStrategy).orElseGet(this::newIncrementingWaitStrategy); } private WaitStrategy newIncrementingWaitStrategy() { return WaitStrategies.incrementingWait( initialSleepTimeAmount, initialSleepTimeUnit, retryIncrementTimeAmount, retryIncrementTimeUnit ); } private StopStrategy determineStopStrategy() { return Optional.ofNullable(stopStrategy) .orElseGet(() -> StopStrategies.stopAfterAttempt(maxAttempts)); } static class LoggingRetryListener implements RetryListener { private static final String RETRY_ATTEMPT_MSG = "Retryer [{}], attempt #{} [delay since first attempt: {} ms]"; private static final String RESULT_MSG = "Result for retryer [{}]: {}"; private static final String EXCEPTION_MESSAGE = "Exception occurred for retryer [{}]: {}: {}"; private final String retryId; private final Level processingLogLevel; private final Level exceptionLogLevel; LoggingRetryListener(String id, Level processingLogLevel, Level exceptionLogLevel) { this.retryId = StringUtils.isBlank(id) ? UUIDs.randomUUIDString() : id; this.processingLogLevel = processingLogLevel; this.exceptionLogLevel = exceptionLogLevel; } /** * @implNote The {@link Attempt} should have either a result or an exception, but we are being safe here * and explicitly checking both of those cases instead of making any assumptions. */ @Override public void onRetry(Attempt attempt) { var attemptNumber = attempt.getAttemptNumber(); RetryLogger.logAttempt(LOG, processingLogLevel, attemptNumber, RETRY_ATTEMPT_MSG, retryId, attemptNumber, attempt.getDelaySinceFirstAttempt()); if (attempt.hasResult()) { logResultAttempt(attempt, attemptNumber); } else if (attempt.hasException()) { logExceptionAttempt(attempt); } } void logResultAttempt(Attempt attempt, long attemptNumber) { RetryLogger.logAttempt(LOG, processingLogLevel, attemptNumber, RESULT_MSG, retryId, attempt.getResult()); } /** * Log all exceptions at exceptionLogLevel (only type and message, not stack trace). * * @implNote the throwable in attempt should never be null, but we guard against just in case */ void logExceptionAttempt(Attempt attempt) { var throwable = attempt.getException(); var type = KiwiThrowables.typeOfNullable(throwable).orElse(null); var message = KiwiThrowables.messageOfNullable(throwable).orElse(null); RetryLogger.logAttempt(LOG, exceptionLogLevel, EXCEPTION_MESSAGE, retryId, type, message); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy