Extra Features

SmallRye Fault Tolerance provides several features that are not present in the MicroProfile Fault Tolerance specification. Note that these features may have experimental status, marked by the @Experimental annotation.

Circuit Breaker Maintenance

It is sometimes useful to see the circuit breaker status from within the application, or reset it to the initial state. This is possible in two steps:

  1. Give the circuit breaker a name by annotating the guarded method with @CircuitBreakerName:

    @ApplicationScoped
    public class MyService {
        @CircuitBreaker
        @CircuitBreakerName("hello-cb") (1)
        public String hello() {
            ...
        }
    }
    1 The circuit breaker guarding the MyService.hello method is given a name hello-cb.
  2. Inject CircuitBreakerMaintenance and call its methods:

    @ApplicationScoped
    public class Example {
        @Inject
        CircuitBreakerMaintenance maintenance;
    
        public void test() {
            System.out.println("Circuit breaker state: "
                + maintenance.currentState("hello-cb")); (1)
            maintenance.resetAll(); (2)
        }
    }
    1 Obtains current circuit breaker state.
    2 Resets all circuit breakers to the initial state.

The CircuitBreakerMaintenance interface provides 3 methods:

  1. currentState(name): returns current state of given circuit breaker. The return type CircuitBreakerState is an enum with 3 values: CLOSED, OPEN, HALF_OPEN.

  2. reset(name): resets given circuit breaker to the initial state.

  3. resetAll(): resets all circuit breakers in the application to the initial state.

@Blocking and @NonBlocking

In addition to the MicroProfile Fault Tolerance @Asynchronous annotation, which can be placed on methods returning Future or CompletionStage, SmallRye Fault Tolerance also supports 2 more annotations for asynchronous processing:

  • @io.smallrye.common.annotation.Blocking

  • @io.smallrye.common.annotation.NonBlocking

These annotations are generic and can have multiple meanings, depending on context. SmallRye Fault Tolerance only pays attention to these annotations if:

  • they are placed on methods that return CompletionStage (the Future type can’t really be used for non-blocking processing);

  • they are placed on methods that apply some fault tolerance strategy (such as @Fallback, defined either on a method or a class).

Under these circumstances, SmallRye Fault Tolerance assigns these annotations the following meaning:

  • @Blocking means that execution of the operation will be offloaded to another thread. In other words, it is an equivalent of @Asynchronous. Use this annotation if the method has blocking logic, but you don’t want to block the caller thread.

    For example:

    @ApplicationScoped
    public class MyService {
        @Retry (1)
        @Blocking (2)
        CompletionStage<String> hello() {
            ...
        }
    }
    1 A fault tolerance annotation. If this wouldn’t be present, SmallRye Fault Tolerance would ignore the @Blocking annotation.
    2 Using the @Blocking annotation, because the method blocks and it is necessary to offload its execution to another thread. With this annotation present, the @Asynchronous annotation is not necessary, and so it is omitted here.

    The thread pool that is used for offloading method calls is provided by the runtime that integrates SmallRye Fault Tolerance.

  • @NonBlocking means that the execution of the operation will not be offloaded to another thread (even if the method is annotated @Asynchronous). Use this annotation if the method doesn’t have blocking logic and you want the execution to stay on the caller thread.

    For example:

    @ApplicationScoped
    public class MyService {
        @Retry (1)
        @NonBlocking (2)
        CompletionStage<String> hello() {
            ...
        }
    }
    1 A fault tolerance annotation. If this wouldn’t be present, SmallRye Fault Tolerance would ignore the @NonBlocking annotation.
    2 Using the @NonBlocking annotation, because the method doesn’t block and offloading execution to another thread is not necessary. With this annotation present, the @Asynchronous annotation is not necessary, and so it is omitted here.

    If the guarded method uses @Retry and some delay between retries is configured, only the initial execution is guaranteed to occur on the original thread. Subsequent attempts may be offloaded to an extra thread, so that the original thread is not blocked on the delay.

    If the guarded method uses @Bulkhead, the execution is not guaranteed to occur on the original thread. If the execution has to wait in the bulkhead queue, it may later end up on a different thread.

    If the original thread is an event loop thread and event loop integration is enabled, then the event loop is always used to execute the guarded method. In such case, all retry attempts and queued bulkhead executions are guaranteed to happen on the original thread.

Additionally, the @Blocking and @NonBlocking annotations may be placed on a class. In that case, they apply to methods satisfying the same criteria: must return CompletionStage and must have some fault tolerance strategy (even if that fault tolerance strategy is declared on the class). An annotation put on a method has priority over an annotation put on a class. For example:

@ApplicationScoped
@NonBlocking
public class MyService {
    @Retry
    CompletionStage<String> hello() { (1)
        ...
    }

    @Retry
    @Blocking
    CompletionStage<String> helloBlocking() { (2)
        ...
    }
}
1 Treated as @NonBlocking, based on the class annotation.
2 Treated as @Blocking, the method annotation has priority over the class annotation.

It is an error to put both @Blocking and @NonBlocking on the same program element.

Rationale

We believe that the @Asynchronous annotation is misnamed, because its meaning is "offload execution to another thread". This isn’t always appropriate in modern asynchronous programming, where methods are often non-blocking and thread offload is not required. We believe that declaring whether the method blocks or not is a better aproach.

At the same time, we designed these annotations to be used by a variety of frameworks, so SmallRye Fault Tolerance can’t eagerly intercept all methods using them. We also want to stay compatible with the MicroProfile Fault Tolerance specification as much as possible. For these reasons, SmallRye Fault Tolerance only considers these annotations for methods that use some fault tolerance strategy.

Recommendation

For methods that use fault tolerance and return CompletionStage, we recommend to declare their @Blocking or @NonBlocking nature. In such case, the @Asynchronous annotation becomes optional.

We also recommend to avoid @Asynchronous methods that return Future, because the only way to obtain the future value is blocking.

Additional Asynchronous Types

MicroProfile Fault Tolerance supports asynchronous fault tolerance for methods that return CompletionStage. (The Future type is not truly asynchronous, so we won’t take it into account here.) SmallRye Fault Tolerance adds support for additional asynchronous types:

  • Mutiny: Uni

  • RxJava: Single, Maybe, Completable

  • Reactor: Mono

These types are treated just like CompletionStage, so everything that works for CompletionStage works for these types as well. Stream-like types (Multi, Observable, Flowable, Flux) are not supported, because their semantics can’t be easily expressed in terms of CompletionStage.

For example:

@ApplicationScoped
public class MyService {
    @Retry
    @NonBlocking (1)
    Uni<String> hello() { (2)
        ...
    }
}
1 Using the @NonBlocking annotation described in @Blocking and @NonBlocking, because the method doesn’t block and offloading execution to another thread is not necessary.
2 Returning the Uni type from Mutiny. This shows that whatever works for CompletionStage also works for the other async types.

The implementation is based on the SmallRye Reactive Converters project. This means that to be able to use any particular asynchronous type, the corresponding converter library must be present. It is possible that the runtime you use already provides the correct integration. Otherwise, add a dependency to your application:

  • Mutiny: io.smallrye.reactive:smallrye-reactive-converter-mutiny

  • RxJava 1: io.smallrye.reactive:smallrye-reactive-converter-rxjava1

  • RxJava 2: io.smallrye.reactive:smallrye-reactive-converter-rxjava2

  • RxJava 3: io.smallrye.reactive:smallrye-reactive-converter-rxjava3

  • Reactor: io.smallrye.reactive:smallrye-reactive-converter-reactor

Quarkus

In Quarkus, the Mutiny converter library is present by default. You can use fault tolerance on methods that return Uni out of the box.

Backoff Strategies for @Retry

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 (with the exception of 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.

Configuration

These annotations may be configured using the same mechanism as MicroProfile Fault Tolerance annotations. For example, to modify the factor of the @ExponentialBackoff annotation above, you can use:

com.example.MyService/hello/ExponentialBackoff/factor=3

Metrics

These annotations do not have any special metrics. All @Retry metrics are still present and reflect the altered behavior.

Non-compatible Mode

In addition to the @Blocking and @NonBlocking annotations, SmallRye Fault Tolerance offers a mode where method asynchrony is determined solely from the its return type. This mode is not compatible with the MicroProfile Fault Tolerance specification (and doesn’t pass 2 tests in the TCK).

This mode is disabled by default. To enable, set the configuration property smallrye.faulttolerance.mp-compatibility to false.

Quarkus

In Quarkus, the non-compatible mode is enabled by default. To restore compatibility, add the following to your application.properties:

smallrye.faulttolerance.mp-compatibility=true

Note that the non-compatible mode is available since SmallRye Fault Tolerance 5.2.0 and Quarkus 2.1.0.Final. Previous versions are always compatible.

In this mode, methods that

  • have some fault tolerance annotation (such as @Retry),

  • return CompletionStage (or some other async type),

  • are not annotated @Blocking, @NonBlocking or @Asynchronous

are not offloaded to a thread pool and still have asynchronous fault tolerance applied. (In other words, they are treated as if they were annotated @NonBlocking.)

For example:

@ApplicationScoped
public class MyService {
    @Retry
    CompletionStage<String> hello() { (1)
        ...
    }

    @Retry
    Uni<String> helloMutiny() { (2)
        ...
    }

    @Retry
    @Blocking
    CompletionStage<String> helloBlocking() { (3)
        ...
    }
}
1 Executed on the original thread, because the method returns CompletionStage. It is as if the method was annotated @NonBlocking.
2 Executed on the original thread, because the method returns an async type. It is as if the method was annotated @NonBlocking.
3 The explicit @Blocking annotation is honored. The method is executed on a thread pool.

Note that the existing annotations still work without a change, both in compatible and non-compatible mode. That is, if a method (or class) is annotated @Asynchronous or @Blocking, execution will be offloaded to a thread pool. If a method (or class) is annotated @NonBlocking, execution will happen on the original thread (even if @Asynchronous is present).

Also note that this mode doesn’t affect methods returning Future. You still have to annotate them @Asynchronous to make sure they are executed on a thread pool. As mentioned in the @Blocking and @NonBlocking section, we discourage using these methods, because the only way to obtain the future value is blocking.