Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

preface

Guava-retrying Github address: github.com/rholder/gua…

Guava-retrying is a small extension of Google’s Guava library that allows you to create configurable retry policies for arbitrary function calls, such as those that talk to remote services with erratic uptime.

In daily development, especially in the era of microservices, when we call an external interface, we often fail to call the interface due to problems such as timeout and traffic limiting of the third-party interface. At this time, we usually retry the interface. Then, how to retry the interface? How many times should I try again? What if you want to set the retry time to more than how long it takes to retry without success? Fortunately, Guava-Retrying provides us with a powerful and easy-to-use retry framework.

I. POM dependence

    <dependency>
      <groupId>com.github.rholder</groupId>
      <artifactId>guava-retrying</artifactId>
      <version>2.0.0</version>
    </dependency>
Copy the code

2. Use examples

We can use the RetryerBuilder to construct a retry. With the RetryerBuilder, we can set when to retry (i.e., when to retry), stop retry policy, failure wait interval policy, and task execution duration limit policy

Let’s start with a simple example:


    private int invokeCount = 0;

    public int realAction(int num) {
        invokeCount++;
        System.out.println(String.format(Num :%d, invokeCount, num));
        if (num <= 0) {
            throw new IllegalArgumentException();
        }
        return num;
    }

    @Test
    public void guavaRetryTest001(a) {
        Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
            // Retry with a non-positive number
            .retryIfRuntimeException()
            // Even numbers are retried
            .retryIfResult(result -> result % 2= =0)
            // Set the maximum execution times to 3
            .withStopStrategy(StopStrategies.stopAfterAttempt(3)).build();

        try {
            invokeCount=0;
            retryer.call(() -> realAction(0));
        } catch (Exception e) {
            System.out.println("Execute 0, exception: + e.getMessage());
        }

        try {
            invokeCount=0;
            retryer.call(() -> realAction(1));
        } catch (Exception e) {
            System.out.println("Execute 1, exception:" + e.getMessage());
        }

        try {
            invokeCount=0;
            retryer.call(() -> realAction(2));
        } catch (Exception e) {
            System.out.println("Execute 2, exception:"+ e.getMessage()); }}Copy the code

Output:

The current attempt is performed for the first time,num:0 The current attempt is performed for the second time,num:0 the current attempt is performed for the third time,num:0 The current attempt is performed for the third time, and the current attempt is performed for the 0 attempt. The current command is executed for the first time,num:1 The current command is executed for the first time,num:2 the current command is executed for the second time,num:2 The current command is executed for the third time,num:2 The current command is executed for the second time, and the command is executed for the second time. Retrying failed to complete successfully after 3 attempts.Copy the code

Three, retry time

The retryIfXXX() method of the RetryerBuilder is used to set the conditions under which retries should be performed, which can generally be divided into retries based on execution exceptions and retries based on method execution results.

3.1 Retry based on exceptions

methods describe
retryIfException() Retry when method execution throws isAssignableFrom Exception.class
retryIfRuntimeException() Retry when method execution throws isAssignableFrom RuntimeException.class
retryIfException(Predicate exceptionPredicate) Here, when an exception occurs, the exception is passed to The exceptionPredicate, so we can use the exception passed in a more customized way to determine when to retry, right
retryIfExceptionOfType(Class<? extends Throwable> exceptionClass) Retry when the method executes an exception that throws isAssignableFrom the passed exceptionClass

3.2 Retry Based on the Command Output

RetryIfResult (@nonnull Predicate resultPredicate) this is relatively simple and retries when the resultPredicate we pass returns true

Stop retry strategy

Stop retry strategy is used to decide when to retry, its interface com. Making. Rholder. Retry. StopStrategy, stop retry strategy implementation classes are com. Making. Rholder. Retry. StopStrategies, It is a policy factory class.

public interface StopStrategy {

    /**
     * Returns <code>true</code> if the retryer should stop retrying.
     *
     * @param failedAttempt the previous failed {@code Attempt}
     * @return <code>true</code> if the retryer must stop, <code>false</code> otherwise
     */
    boolean shouldStop(Attempt failedAttempt);
}
Copy the code

4.1 NeverStopStrategy

This policy will always retry, never stop, look at its implementation class and return false directly

        @Override
        public boolean shouldStop(Attempt failedAttempt) {
            return false;
        }
Copy the code

4.2 StopAfterAttemptStrategy

When the retry is stopped after the specified number of executions, look at its implementation class:

    private static final class StopAfterAttemptStrategy implements StopStrategy {
        private final int maxAttemptNumber;

        public StopAfterAttemptStrategy(int maxAttemptNumber) {
            Preconditions.checkArgument(maxAttemptNumber >= 1."maxAttemptNumber must be >= 1 but is %d", maxAttemptNumber);
            this.maxAttemptNumber = maxAttemptNumber;
        }

        @Override
        public boolean shouldStop(Attempt failedAttempt) {
            returnfailedAttempt.getAttemptNumber() >= maxAttemptNumber; }}Copy the code

4.3 StopAfterDelayStrategy

When the first execution of the distance method exceeds the specified delay time, the method will stop, that is, continue to retry. When the next retry is performed, the method will determine whether the time spent from the first execution to the present is longer than the specified delay time.

   private static final class StopAfterAttemptStrategy implements StopStrategy {
        private final int maxAttemptNumber;

        public StopAfterAttemptStrategy(int maxAttemptNumber) {
            Preconditions.checkArgument(maxAttemptNumber >= 1."maxAttemptNumber must be >= 1 but is %d", maxAttemptNumber);
            this.maxAttemptNumber = maxAttemptNumber;
        }

        @Override
        public boolean shouldStop(Attempt failedAttempt) {
            returnfailedAttempt.getAttemptNumber() >= maxAttemptNumber; }}Copy the code

WaitStrategy and BlockStrategy of retry interval

Taken together, these two policies are used to control the interval between retry tasks and how tasks block while waiting for the interval. That is, WaitStrategy determines how long the retry task will wait before the next task is executed, and BlockStrategy determines how the task will wait. They the strategy of two factories respectively. Com. Making rholder. Retry. WaitStrategies and BlockStrategies.

5.1 BlockStrategy

5.1.1 ThreadSleepStrategy

BlockStrategies determine how to block tasks by ** thread.sleep ()**

    @Immutable
    private static class ThreadSleepStrategy implements BlockStrategy {

        @Override
        public void block(long sleepTime) throws InterruptedException { Thread.sleep(sleepTime); }}Copy the code

5.2 WaitStrategy

5.2.1 IncrementingWaitStrategy

When determining the task interval, the policy returns an increasing interval, that is, the task retry interval increases gradually and becomes longer. Check its implementation:

    private static final class IncrementingWaitStrategy implements WaitStrategy {
        private final long initialSleepTime;
        private final long increment;

        public IncrementingWaitStrategy(long initialSleepTime,
                                        long increment) {
            Preconditions.checkArgument(initialSleepTime >= 0L."initialSleepTime must be >= 0 but is %d", initialSleepTime);
            this.initialSleepTime = initialSleepTime;
            this.increment = increment;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            long result = initialSleepTime + (increment * (failedAttempt.getAttemptNumber() - 1));
            return result >= 0L ? result : 0L; }}Copy the code

The policy enters a starting interval value and an incrementing step, then increments the wait duration each time.

5.2.2 RandomWaitStrategy

As the name implies, return a random interval. What we need to pass in is a minimum interval and a maximum interval, and then return a random interval between the two, which is implemented as follows:

    private static final class RandomWaitStrategy implements WaitStrategy {
        private static final Random RANDOM = new Random();
        private final long minimum;
        private final long maximum;

        public RandomWaitStrategy(long minimum, long maximum) {
            Preconditions.checkArgument(minimum >= 0."minimum must be >= 0 but is %d", minimum);
            Preconditions.checkArgument(maximum > minimum, "maximum must be > minimum but maximum is %d and minimum is", maximum, minimum);

            this.minimum = minimum;
            this.maximum = maximum;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            long t = Math.abs(RANDOM.nextLong()) % (maximum - minimum);
            returnt + minimum; }}Copy the code

5.2.3 requires FixedWaitStrategy

The policy is to return a fixed retry interval. See the implementation:

    private static final class FixedWaitStrategy implements WaitStrategy {
        private final long sleepTime;

        public FixedWaitStrategy(long sleepTime) {
            Preconditions.checkArgument(sleepTime >= 0L."sleepTime must be >= 0 but is %d", sleepTime);
            this.sleepTime = sleepTime;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            returnsleepTime; }}Copy the code

5.2.4 ExceptionWaitStrategy

This policy is based on method execution exceptions to determine whether and how long to wait between retries.

    private static final class ExceptionWaitStrategy<T extends Throwable> implements WaitStrategy {
        private final Class<T> exceptionClass;
        private final Function<T, Long> function;

        public ExceptionWaitStrategy(@Nonnull Class<T> exceptionClass, @Nonnull Function<T, Long> function) {
            this.exceptionClass = exceptionClass;
            this.function = function;
        }

        @SuppressWarnings({"ThrowableResultOfMethodCallIgnored", "ConstantConditions", "unchecked"})
        @Override
        public long computeSleepTime(Attempt lastAttempt) {
            if (lastAttempt.hasException()) {
                Throwable cause = lastAttempt.getExceptionCause();
                if (exceptionClass.isAssignableFrom(cause.getClass())) {
                    returnfunction.apply((T) cause); }}return 0L; }}Copy the code

5.2.5 CompositeWaitStrategy

This is nothing to say, as the name suggests, but a combination of policies, you can pass in multiple WaitStrategies, and then the total interval returned by all WaitStrategies is the final interval. See the implementation:

   private static final class CompositeWaitStrategy implements WaitStrategy {
        private final List<WaitStrategy> waitStrategies;

        public CompositeWaitStrategy(List<WaitStrategy> waitStrategies) { Preconditions.checkState(! waitStrategies.isEmpty(),"Need at least one wait strategy");
            this.waitStrategies = waitStrategies;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            long waitTime = 0L;
            for (WaitStrategy waitStrategy : waitStrategies) {
                waitTime += waitStrategy.computeSleepTime(failedAttempt);
            }
            returnwaitTime; }}Copy the code

5.2.6 FibonacciWaitStrategy

This strategy is similar to IncrementingWaitStrategy in that the interval increments as the number of retries increases, except that the FibonacciWaitStrategy is calculated as a Fibonacci sequence. Using this strategy, We need to pass in a multiplier factor and a maximum interval, and the implementation will be irrelevant

5.2.7 ExponentialWaitStrategy

This is similar to the IncrementingWaitStrategy and FibonacciWaitStrategy in that the interval increases with the number of retries, but this strategy increases exponentially. See the implementation:

    private static final class ExponentialWaitStrategy implements WaitStrategy {
        private final long multiplier;
        private final long maximumWait;

        public ExponentialWaitStrategy(long multiplier,
                                       long maximumWait) {
            Preconditions.checkArgument(multiplier > 0L."multiplier must be > 0 but is %d", multiplier);
            Preconditions.checkArgument(maximumWait >= 0L."maximumWait must be >= 0 but is %d", maximumWait);
            Preconditions.checkArgument(multiplier < maximumWait, "multiplier must be < maximumWait but is %d", multiplier);
            this.multiplier = multiplier;
            this.maximumWait = maximumWait;
        }

        @Override
        public long computeSleepTime(Attempt failedAttempt) {
            double exp = Math.pow(2, failedAttempt.getAttemptNumber());
            long result = Math.round(multiplier * exp);
            if (result > maximumWait) {
                result = maximumWait;
            }
            return result >= 0L ? result : 0L; }}Copy the code

The RetryListener is RetryListener

When a retry occurs, the onRetry method of RetryListener is called, at which point we can perform additional operations such as logging.

    public int realAction(int num) {
        if (num <= 0) {
            throw new IllegalArgumentException();
        }
        return num;
    }

    @Test
    public void guavaRetryTest001(a) throws ExecutionException, RetryException {
        Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder().retryIfException()
            .withRetryListener(new MyRetryListener())
            // Set the maximum execution times to 3
            .withStopStrategy(StopStrategies.stopAfterAttempt(3)).build();
        retryer.call(() -> realAction(0));

    }

    private static class MyRetryListener implements RetryListener {

        @Override
        public <V> void onRetry(Attempt<V> attempt) {
            System.out.println("The first" + attempt.getAttemptNumber() + "Secondary execution"); }}Copy the code

Output:

Perform the first operation. Perform the second operation. Perform the third operationCopy the code

Seven, retry principle

At this point, the implementation principle is probably clear, which is the combination of the above strategies to achieve a very flexible retry mechanism. Attempt to do STH

public interface Attempt<V> {
    public V get(a) throws ExecutionException;

    public boolean hasResult(a);
    
    public boolean hasException(a);

    public V getResult(a) throws IllegalStateException;

    public Throwable getExceptionCause(a) throws IllegalStateException;

    public long getAttemptNumber(a);

    public long getDelaySinceFirstAttempt(a);
}
Copy the code

The Attempt class contains the number of times a task has been executed, the exception is executed, the result is executed, and the time interval since the first execution of the task. Therefore, the subsequent retry timing and other policies are determined based on this value.

Let’s look at the key entry Retryer#call:

    public V call(Callable<V> callable) throws ExecutionException, RetryException {
        long startTime = System.nanoTime();
        
        // The number of executions starts from 1
        for (int attemptNumber = 1; ; attemptNumber++) {
            Attempt<V> attempt;
            try {
                // Try to execute
                V result = attemptTimeLimiter.call(callable);
                
                // If the execution succeeds, the result is encapsulated as ResultAttempt
                attempt = new Retryer.ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            } catch (Throwable t) {
                ExceptionAttempt Encapsulates the result as ExceptionAttempt
                attempt = new Retryer.ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            }

            // Here we pass the execution result to RetryListener to do something extra
            for (RetryListener listener : listeners) {
                listener.onRetry(attempt);
            }

            // This is where the decision is made whether to retry or not. If no retry is performed, an exception is returned on success and an exception is returned on failure
            if(! rejectionPredicate.apply(attempt)) {return attempt.get();
            }
            
            If this parameter is displayed, the system needs to retry. In this case, the system determines whether the retry time is reached. If so, an exception is returned
            if (stopStrategy.shouldStop(attempt)) {
                throw new RetryException(attemptNumber, attempt);
            } else {
                // Determine the retry interval
                long sleepTime = waitStrategy.computeSleepTime(attempt);
                try {
                    // Block
                    blockStrategy.block(sleepTime);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw newRetryException(attemptNumber, attempt); }}}Copy the code

Eight, summary

As you can see from the whole article, the core implementation is not difficult, but the framework provides a very clear and flexible retry mechanism through the combination of builder pattern and strategy pattern. Its design ideas are worth learning from!