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

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

Go to download

A Java library to allow for the creation of configurable retrying strategies for an arbitrary function call, such as something that communicates with a remote service with flaky uptime.

There is a newer version: 2.1.3
Show newest version
/*
 * Copyright 2012-2015 Ray Holder
 * Modifications copyright 2017-2018 Robert Huffman
 * Modifications copyright 2020-2021 Kiwi Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.kiwiproject.retry;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.kiwiproject.retry.Attempt.newExceptionAttempt;
import static org.kiwiproject.retry.Attempt.newResultAttempt;

import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

/**
 * A retryer, which executes a call, and retries it until it succeeds, or
 * a stop strategy decides to stop retrying. A wait strategy is used to sleep
 * between attempts. The strategy to decide if the call succeeds or not is
 * also configurable.
 * 

* A retryer can also wrap the callable into a RetryerCallable, which can be submitted to an executor. *

* Retryer instances are better constructed with a {@link RetryerBuilder}. A retryer * is thread-safe, provided the arguments passed to its constructor are thread-safe. */ public final class Retryer { private final StopStrategy stopStrategy; private final WaitStrategy waitStrategy; private final BlockStrategy blockStrategy; private final AttemptTimeLimiter attemptTimeLimiter; private final List>> retryPredicates; private final Collection listeners; /** * @param attemptTimeLimiter to prevent from any single attempt from spinning infinitely * @param stopStrategy the strategy used to decide when the retryer must stop retrying * @param waitStrategy the strategy used to decide how much time to sleep between attempts * @param blockStrategy the strategy used to decide how to block between retry attempts; * eg, Thread#sleep(), latches, etc. * @param retryPredicates the predicates used to decide if the attempt must be retried (without * regard to the StopStrategy). * @param listeners collection of retry listeners */ Retryer(@Nonnull AttemptTimeLimiter attemptTimeLimiter, @Nonnull StopStrategy stopStrategy, @Nonnull WaitStrategy waitStrategy, @Nonnull BlockStrategy blockStrategy, @Nonnull List>> retryPredicates, @Nonnull Collection listeners) { checkNotNull(attemptTimeLimiter, "timeLimiter may not be null"); checkNotNull(stopStrategy, "stopStrategy may not be null"); checkNotNull(waitStrategy, "waitStrategy may not be null"); checkNotNull(blockStrategy, "blockStrategy may not be null"); checkNotNull(retryPredicates, "retryPredicates may not be null"); checkNotNull(listeners, "listeners may not null"); this.attemptTimeLimiter = attemptTimeLimiter; this.stopStrategy = stopStrategy; this.waitStrategy = waitStrategy; this.blockStrategy = blockStrategy; this.retryPredicates = retryPredicates; this.listeners = listeners; } /** * Executes the given callable, retrying if necessary. If the retry predicate * accepts the attempt, the stop strategy is used to decide if a new attempt * must be made. Then the wait strategy is used to decide how much time to sleep * and a new attempt is made. * * @param callable the callable task to be executed * @param the return type of the Callable * @return the computed result of the given callable * @throws RetryException if all the attempts failed before the stop strategy decided to abort * @throws InterruptedException If this thread is interrupted. This can happen because * {@link Thread#sleep} is invoked between attempts */ public T call(Callable callable) throws RetryException, InterruptedException { long startTimeNanos = System.nanoTime(); for (var attemptNumber = 1; ; attemptNumber++) { var attempt = call(callable, startTimeNanos, attemptNumber); listeners.forEach(listener -> safeInvokeListener(listener, attempt)); if (!shouldRetry(attempt)) { return getOrThrow(attempt); } if (stopStrategy.shouldStop(attempt)) { throw new RetryException(attempt); } else { long sleepTime = waitStrategy.computeSleepTime(attempt); blockStrategy.block(sleepTime); } } } private Attempt call(Callable callable, long startTimeNanos, int attemptNumber) throws InterruptedException { try { T result = attemptTimeLimiter.call(callable); return newResultAttempt(result, attemptNumber, computeMillisSince(startTimeNanos)); } catch (InterruptedException e) { throw e; } catch (Exception e) { return newExceptionAttempt(e, attemptNumber, computeMillisSince(startTimeNanos)); } } private static long computeMillisSince(long startTimeNanos) { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTimeNanos); } private static void safeInvokeListener(RetryListener listener, Attempt attempt) { try { listener.onRetry(attempt); } catch (Exception exception) { // intentionally ignored per the API Note in RetryListener#onRetry } } /** * Executes the given runnable, retrying if necessary. If the retry predicate * accepts the attempt, the stop strategy is used to decide if a new attempt * must be made. Then the wait strategy is used to decide how much time to sleep * and a new attempt is made. * * @param runnable the runnable task to be executed * @throws RetryException if all the attempts failed before the stop strategy decided * to abort * @throws InterruptedException If this thread is interrupted. This can happen because * {@link Thread#sleep} is invoked between attempts */ public void run(Runnable runnable) throws RetryException, InterruptedException { call(() -> { runnable.run(); return null; }); } /** * Throw the Attempt's exception, if it has one, wrapped in a RetryException. Otherwise, * return the attempt's result. * * @param attempt An attempt that was made by invoking the call * @param The type of the attempt * @return The result of the attempt * @throws RetryException If the attempt has an exception */ private T getOrThrow(Attempt attempt) throws RetryException { if (attempt.hasException()) { throw new RetryException(attempt); } return attempt.getResult(); } /** * Applies the retry predicates to the attempt, in order, until either one * predicate returns true or all predicates return false. * * @param attempt The attempt made by invoking the call */ private boolean shouldRetry(Attempt attempt) { for (Predicate> predicate : retryPredicates) { if (predicate.test(attempt)) { return true; } } return false; } /** * Wraps the given {@link Callable} in a {@link RetryerCallable}, which can * be submitted to an executor. The returned {@link RetryerCallable} uses * this {@link Retryer} instance to call the given {@link Callable}. * * @param callable the callable to wrap * @param the return type of the Callable * @return a {@link RetryerCallable} that behaves like the given {@link Callable} with retry behavior defined by this {@link Retryer} */ public RetryerCallable wrap(Callable callable) { return new RetryerCallable<>(this, callable); } /** * A {@link Callable} which wraps another {@link Callable} in order to add * retrying behavior from a given {@link Retryer} instance. */ public static class RetryerCallable implements Callable { private final Retryer retryer; private final Callable callable; private RetryerCallable(Retryer retryer, Callable callable) { this.retryer = retryer; this.callable = callable; } /** * Makes the enclosing retryer call the wrapped callable. * * @see Retryer#call(Callable) */ @Override public T call() throws Exception { return retryer.call(callable); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy