Retry

For introduction, see How to Retry on Failures.

Description

If a guarded method throws an exception, retry will swallow that exception and call the method again.

More specifically:

  1. If the guarded method returns normally, its return value is returned.

  2. Otherwise, if the guarded method throws an exception whose type is assignable to any of the abortOn types (see abortOn), retrying is aborted and the exception is rethrown.

  3. Otherwise, if the guarded method throws an exception whose type is assignable to any of the retryOn types (see retryOn), the method call is retried.

  4. Otherwise, the exception is rethrown.

Interactions with Other Strategies

See How to Use Multiple Strategies for an overview of how fault tolerance strategies are nested.

If @Fallback is used with @Retry, the fallback method or handler may be invoked if all retry attempts failed, depending on the fallback configuration.

Configuration

There are 9 configuration options, corresponding to the 9 members of the @Retry annotation.

maxRetries

Type: int

Default: 3

The maximum number of retries. -1 means the maximum number of retries is unlimited.

delay + delayUnit

Type: long + ChronoUnit

Default: 0 millis

The delay between retry attempts.

maxDuration + durationUnit

Type: long + ChronoUnit

Default: 180_000 millis, or 3 minutes

The maximum duration for which retries will be attempted.

Note the naming inconsistency between maxDuration and durationUnit. It is common for the time unit property to be named after the time value property, e.g. maxDurationUnit. Fixing this inconsistency would be a breaking change the MicroProfile Fault Tolerance specification is not willing to make.

jitter + jitterDelayUnit

Type: long + ChronoUnit

Default: 200 millis

The maximum jitter to adjust the delay between retries. If set, the delay before each retry attempt will be computed as follows:

  1. Generate random number from the closed interval [-jitter, jitter].

  2. Add the generated random number to the configured delay.

  3. If the result is by any chance negative, use 0.

Note the naming inconsistency between jitter and jitterDelayUnit. It is common for the time unit property to be named after the time value property, e.g. jitterUnit. Fixing this inconsistency would be a breaking change the MicroProfile Fault Tolerance specification is not willing to make.

retryOn

Type: Class<? extends Throwable>[]

Default: {Exception.class}

Set of exception types that trigger a retry.

abortOn

Type: Class<? extends Throwable>[]

Default: {}

Set of exception types that abort retrying. This configuration takes precedence over retryOn.

Metrics

Retry exposes the following metrics:

Name

ft.retry.calls.total

Type

Counter

Unit

None

Description

The number of times the retry logic was run. This will always be once per method call.

Tags

  • method - the fully qualified method name

  • retried = [true|false] - whether any retries occurred

  • retryResult = [valueReturned|exceptionNotRetryable|maxRetriesReached|maxDurationReached] - the reason that last attempt to call the method was not retried

Name

ft.retry.retries.total

Type

Counter

Unit

None

Description

The number of times the method was retried

Tags

  • method - the fully qualified method name

See the Metrics reference guide for general metrics information.

Extra Features

Backoff Strategies for @Retry

This is an additional feature of SmallRye Fault Tolerance and is not specified by MicroProfile Fault Tolerance.

When retrying failed operations, it is often useful to make a delay between retry attempts. This delay is also called "backoff". The @Retry annotation in MicroProfile Fault Tolerance supports a single backoff strategy: constant. That is, the delay between all retry attempts is identical (except for a random jitter).

SmallRye Fault Tolerance offers 3 annotations to specify a different backoff strategy:

  • @ExponentialBackoff

  • @FibonacciBackoff

  • @CustomBackoff

One of these annotations may be present on any program element (method or class) that also has the @Retry annotation. For example:

package com.example;

@ApplicationScoped
public class MyService {
    @Retry
    @ExponentialBackoff
    public void hello() {
        ...
    }
}

It is an error to add a backoff annotation to a program element that doesn’t have @Retry (e.g. add @Retry on a class and @ExponentialBackoff on a method). It is also an error to add more than one of these annotations to the same program element.

When any one of these annotations is present, it modifies the behavior specified by the @Retry annotation. The new behavior is as follows:

For @ExponentialBackoff, the delays between retry attempts grow exponentially, using a defined factor. By default, the factor is 2, so each delay is 2 * the previous delay. For example, if the initial delay (specified by @Retry) is 1 second, then the second delay is 2 seconds, third delay is 4 seconds, fourth delay is 8 seconds etc. It is possible to define a maxDelay, so that this growth has a limit.

For @FibonacciBackoff, the delays between retry attempts grow per the Fibonacci sequence. For example, if the initial delay (specified by @Retry) is 1 second, then the second delay is 2 seconds, third delay is 3 seconds, fourth delay is 5 seconds etc. It is possible to define a maxDelay, so that this growth has a limit.

Both @ExponentialBackoff and @FibonacciBackoff also apply jitter, exactly like plain @Retry.

Also, since @Retry has a default maxDuration of 3 minutes and default maxRetries of 3, both @ExponentialBackoff and @FibonacciBackoff define a maxDelay of 1 minute. If we redefine maxRetries to a much higher value, and the guarded method keeps failing, the delay would eventually become higher than 1 minute. In that case, it will be limited to 1 minute. Of course, maxDelay can be configured. If set to 0, there’s no limit, and the delays will grow without bounds.

For @CustomBackoff, computing the delays between retry attempts is delegated to a specified implementation of CustomBackoffStrategy. This is an advanced option.

For more information about these backoff strategies, see the javadoc of the annotations.

Predicate-Based @Retry

This is an additional feature of SmallRye Fault Tolerance and is not specified by MicroProfile Fault Tolerance.

By default, an operation is retried only if it fails and the exception is assignable to one of the classes defined in @Retry.retryOn (and not assignable to any of the classes defined in @Retry.abortOn). With the @RetryWhen annotation, it is possible to instead define a custom predicate for the exception, as well as a predicate for the result.

The @RetryWhen annotation may be present on any program element (method or class) that also has the @Retry annotation. For example:

package com.example;

@ApplicationScoped
public class MyService {
    @Retry
    @RetryWhen(
        result = IsNull.class, (1)
        exception = IsRuntimeException.class (2)
    )
    public String hello() {
        ...
    }

    public static final class IsNull implements Predicate<Object> {
        @Override
        public boolean test(Object o) {
            return o == null;
        }
    }

    public static final class IsRuntimeException implements Predicate<Throwable> {
        @Override
        public boolean test(Throwable throwable) {
            return throwable instanceof RuntimeException;
        }
    }
}
1 When the method returns null, it will be retried.
2 When the method throws a RuntimeException, it will be retried.

All other results are considered expected and are not retried.

It is an error to specify @RetryWhen.exception if @Retry.retryOn / @Retry.abortOn is specified. Specifying @RetryWhen.result together with @Retry.retryOn / @Retry.abortOn is possible, although perhaps not the best idea.

It is an error to add a @RetryWhen annotation to a program element that doesn’t have @Retry (e.g. add @Retry on a class and @RetryWhen on a method).

For more information about @RetryWhen, see the javadoc of the annotation.

Inspecting Exception Cause Chains

This is an additional feature of SmallRye Fault Tolerance and is not specified by MicroProfile Fault Tolerance.
This feature only works in the non-compatible mode.

The @Retry annotation can specify that certain exceptions should be treated as failures (retryOn) and others as successes (abortOn). The specification limits this to inspecting the actual exception that was thrown. However, in many cases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain.

For that reason, in the non-compatible mode, if the actual thrown exception isn’t known failure or known success, SmallRye Fault Tolerance inspects the cause chain. To be specific, in case a @Retry method throws an exception, the decision process is:

  1. If the abortOn exceptions are not default and the exception is assignable to one of the abortOn exceptions, retry is aborted and the exception is rethrown.

  2. Otherwise, if the retryOn exceptions are not default and the exception is assignable to one of the retryOn exceptions, retry is attempted.

  3. Otherwise, if the exception is assignable to one of the abortOn exceptions or its cause chain contains an exception assignable to one of the abortOn exceptions, retry is aborted and the exception is rethrown.

  4. Otherwise, if the exception is assignable to one of the retryOn exceptions or its cause chain contains an exception assignable to one of the retryOn exceptions, retry is attempted.

  5. Otherwise, the exception is rethrown.

For example:

@Retry(maxRetries = 5,
    abortOn = ExpectedOutcomeException.class, (1)
    retryOn = IOException.class) (2)
public Result doSomething() {
    ...
}
1 If doSomething throws an ExpectedOutcomeException, or a WrapperException whose cause is ExpectedOutcomeException, retry is aborted and the exception is thrown.
2 If doSomething throws an IOException, or a WrapperException whose cause is IOException, retry is attempted.
@Retry(maxRetries = 5,
    abortOn = ExpectedOutcomeException.class) (1) (2)
public Result doSomething() {
    ...
}
1 If doSomething throws an ExpectedOutcomeException, or a WrapperException whose cause is ExpectedOutcomeException, retry is aborted and the exception is thrown.
2 There’s no retryOn, so the 2nd step in the algorithm above is skipped. This is what turns the WrapperException whose cause is ExpectedOutcomeException into an aborted retry.