Java development tool retry framework guava retrying

Time:2022-6-1

preface

Guava retrying GitHub address:https://github.com/rholder/guava-retrying

Guava retrying is a small extension of Google’s guava library, which allows the creation of configurable retry policies for arbitrary function calls, such as function calls that talk to remote services with unstable uptime.

In daily development, especially in the era of the prevalence of micro services, when we call an external interface, we often fail to call the interface because of the timeout and flow restriction of the third-party interface. At this time, we usually retry the interface. How to retry the problem? How many times should I try again? What should I do if I want to set the retry time after which the retry is unsuccessful? Fortunately, guava retrying provides us with a powerful and easy-to-use retry framework, guava retrying.

1、 POM dependency

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

2、 Use examples

We can passRetryerBuilderTo construct a retrier throughRetryerBuilderYou can set when to retry (i.e. retry opportunity), stop retry policy, failure waiting interval policy, and task execution time limit policy

Let’s start with a simple example:

private int invokeCount = 0;

    public int realAction(int num) {
        invokeCount++;
        System. out. Println (string.format ("currently executing the%d time, num:%d", invokecount, Num));
        if (num <= 0) {
            throw new IllegalArgumentException();
        }
        return num;
    }

    @Test
    public void guavaRetryTest001() {
        Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder()
            //Non positive number for retry
            .retryIfRuntimeException()
            //Even number retry
            .retryIfResult(result -> result % 2 == 0)
            //Set the maximum number of executions 3 times
            .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());
        }
    }

Output:

The first time of current execution, num:0
Current execution for the second time, num:0
The 3rd time of current execution, num:0
Executing 0, exception: retrying failed to complete successfully after 3 attempts
The first time of current execution, num:1
The first time of current execution, num:2
The second time of current execution, num:2
Current execution for the third time, num:2
Executing 2, exception: retrying failed to complete successfully after 3 attempts

3、 Retry opportunity

Retryerbuilder’sretryIfXXX()Method to setWhen to retryIn general, it can be divided intoRetry based on execution exceptionandRetry based on method execution resultsTwo categories.

3.1 retry based on exceptions

method describe
retryIfException() When the method executes, an exception isassignablefrom exception is thrown Class and try again
retryIfRuntimeException() An exception isassignablefrom runtimeException is thrown when the method executes Class and try again
retryIfException(Predicate<Throwable> exceptionPredicate) Here, when an exception occurs, the exception will be passed to exceptionpredicate. Then we can decide when to retry through a more customized method for the incoming exception
retryIfExceptionOfType(Class<? extends Throwable> exceptionClass) Retry when the method execution throws an exception isassignablefrom the passed exceptionclass

3.2 retry according to the returned results

retryIfResult(@Nonnull Predicate<V> resultPredicate)
This is relatively simple. When the resultpredicate we passed in returns true, we will retry

4、 Stop retry policy stopstrategy

The stop retry policy is used to decide when not to retry. Its interface is com github. rholder. retry. Stopstrategy and the implementation classes of stop retry policy are all on com github. rholder. retry. Stopstrategies, which 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);
}

4.1 NeverStopStrategy

This policy will always try again, never stop, view its implementation class, and directly return false

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

4.2 StopAfterAttemptStrategy

When the number of executions reaches the specified number, stop retrying and view 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) {
            return failedAttempt.getAttemptNumber() >= maxAttemptNumber;
        }
    }

4.3 StopAfterDelayStrategy

Stop when the first execution of the distance method exceeds the specified delay time, that is, retry all the time. When the next retry is performed, judge whether the time consumed from the first execution to the present exceeds the specified delay time. Check the implementation:

   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) {
            return failedAttempt.getAttemptNumber() >= maxAttemptNumber;
        }
    }

5、 Retry interval policy waitstrategy and retry blocking policy blockstrategy

Put together, these two strategies are used to control the interval between retry tasks and how tasks block when waiting for the interval. That is to say, the waitstrategy determines how long the retry task waits before the next task is executed. The blockstrategy is used to determine how the task waits. Their strategic factories are com github. rholder. retry. Waitstrategies and blockstrategies.

5.1 BlockStrategy

5.1.1 ThreadSleepStrategy

This is blockstrategies, which determines how to block tasks, mainly throughThread.sleep()To block and view its implementation:

    @Immutable
    private static class ThreadSleepStrategy implements BlockStrategy {

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

5.2 WaitStrategy

5.2.1 IncrementingWaitStrategy

When determining the task interval, the policy returns an increasing interval, that is, the retry interval of each task increases gradually and becomes longer. Check the 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;
        }
    }

The policy inputs a starting interval value and an increasing step length, and then each waiting time is increased by the increasing time length.

5.2.2 RandomWaitStrategy

As the name suggests, a random interval duration is returned. What we need to pass in is a minimum interval and a maximum interval, and then a random interval duration between the two is returned. Its implementation is:

    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);
            return t + minimum;
        }
    }

5.2.3 FixedWaitStrategy

The policy is to return a fixed retry interval. View its 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) {
            return sleepTime;
        }
    }

5.2.4 ExceptionWaitStrategy

This policy determines whether to wait between retry tasks and how long the interval is based on the method execution exception.

    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())) {
                    return function.apply((T) cause);
                }
            }
            return 0L;
        }
    }

5.2.5 CompositeWaitStrategy

There is nothing to say about this. Just as the name suggests, it is a combination of policies. You can pass in multiple waitstrategy, and the final interval time is the sum of the interval length returned by all waitstrategy. View its 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);
            }
            return waitTime;
        }
    }

5.2.6 FibonacciWaitStrategy

This strategy is somewhat similar to incrementingwaitstrategy. The interval time increases with the number of retries. The difference is that fibonacciwaitstrategy is calculated according to the Fibonacci sequence. When using this strategy, we need to pass in a multiplier factor and the maximum interval length. Its implementation will not be posted

5.2.7 ExponentialWaitStrategy

Similar to incrementingwaitstrategy and fibonacciwaitstrategy, the interval increases with the number of retries, but the policy increases exponentially. View its 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;
        }
    }

6、 Retry listener retrylistener

When a retry occurs, the onretry method of retrylistener will be called. At this time, 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() throws ExecutionException, RetryException {
        Retryer<Integer> retryer = RetryerBuilder.<Integer>newBuilder().retryIfException()
            .withRetryListener(new MyRetryListener())
            //Set the maximum number of executions 3 times
            .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" + attempt.getattemptnumber() + "second execution");
        }
    }

Output:

1st execution
2nd execution
3rd execution

7、 Retry principle

In fact, after this step, the implementation principle is probably very clear. It is the combination of the above strategies to achieve a very flexible retry mechanism. Before that, let’s look at an attempt

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

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

    public V getResult() throws IllegalStateException;

    public Throwable getExceptionCause() throws IllegalStateException;

    public long getAttemptNumber();

    public long getDelaySinceFirstAttempt();
}

Through the interface method, we can know that the attemptclass contains the task execution times, task execution exceptions, task execution results, and the time interval between the first execution of the task and the present. Then our subsequent retry opportunities and other strategies are determined based on this value.

Next, let’s look at the key implementation entry retryer\call:

public V call(Callable<V> callable) throws ExecutionException, RetryException {
        long startTime = System.nanoTime();
        
        //Execution times start from 1
        for (int attemptNumber = 1; ; attemptNumber++) {
            Attempt<V> attempt;
            try {
                //Attempt to execute
                V result = attemptTimeLimiter.call(callable);
                
                //If the execution is successful, the result is encapsulated as resultattempt
                attempt = new Retryer.ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            } catch (Throwable t) {
                //If an exception is executed, the result is encapsulated as exceptionattempt
                attempt = new Retryer.ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
            }

            //Here, the execution result is passed to retrylistener to do some extra things
            for (RetryListener listener : listeners) {
                listener.onRetry(attempt);
            }

            //This is where you decide whether to retry. If you do not retry, the result will be returned directly. If the execution succeeds, the result will be returned. If the execution fails, the exception will be returned
            if (!rejectionPredicate.apply(attempt)) {
                return attempt.get();
            }
            
            //So far, it indicates that retry is required. At this time, it is first decided whether the time to stop retry has arrived. If so, an exception is returned directly
            if (stopStrategy.shouldStop(attempt)) {
                throw new RetryException(attemptNumber, attempt);
            } else {
                //Determine retry interval
                long sleepTime = waitStrategy.computeSleepTime(attempt);
                try {
                    //Block
                    blockStrategy.block(sleepTime);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RetryException(attemptNumber, attempt);
                }
            }
        }

8、 Summary

It can be seen from the whole article that the core implementation is not difficult, but this framework provides a very clear and flexible retry mechanism through the combination of builder mode and strategy mode. Its design idea is still worth learning from!