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:
-
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 namehello-cb
. -
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 4 methods:
-
currentState(name)
: returns current state of given circuit breaker. The return typeCircuitBreakerState
is anenum
with 3 values:CLOSED
,OPEN
,HALF_OPEN
.-
onStateChange(name, callback)
: registers a callback that will be called when given circuit breaker changes state.
-
-
reset(name)
: resets given circuit breaker to the initial state. -
resetAll()
: resets all circuit breakers in the application to the initial state.
See the javadoc of those methods for more information.
@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
(theFuture
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 approach.
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 declaring their @Blocking
or @NonBlocking
nature.
In such case, the @Asynchronous
annotation becomes optional.
We also recommend avoiding @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
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
) 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 internally converts the async types to a CompletionStage
and back.
This means that to be able to use any particular asynchronous type, the corresponding converter must be present.
SmallRye Fault Tolerance provides support libraries for popular asynchronous types, and these support libraries include the corresponding converters.
It is possible that the runtime you use already provides the correct integration. Otherwise, add a dependency to your application:
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.
Non-compatible Mode
SmallRye Fault Tolerance offers a mode where certain features are improved beyond specification, as described below. This mode is not compatible with the MicroProfile Fault Tolerance specification (and doesn’t necessarily pass the entire TCK).
This mode is disabled by default.
To enable, set the configuration property smallrye.faulttolerance.mp-compatibility
to false
.
Determining Asynchrony from Method Signature
In the non-compatible mode, method asynchrony is determined solely from its signature. That is, methods that
-
have some fault tolerance annotation (such as
@Retry
), -
return
CompletionStage
(or some other async type),
always have asynchronous fault tolerance applied.
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 doesn’t affect methods returning Future
.
You still have to annotate them @Asynchronous
to make sure they are executed on a thread pool and are guarded properly.
As mentioned in the @Blocking and @NonBlocking section, we discourage using these methods, because the only way to obtain the future value is blocking.
Inspecting Exception Cause Chains
The @CircuitBreaker
, @Fallback
and @Retry
annotations can be used to specify that certain exceptions should be treated as failures and others as successes.
This is limited to inspecting the actual exception that was thrown.
However, in many usecases, exceptions are wrapped and the exception the user wants to decide on is only present in the cause chain.
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 @Fallback
method throws an exception, the decision process is:
-
if the exception is assignable to one of the
skipOn
exceptions, fallback is skipped and the exception is rethrown; -
otherwise, if the exception is assignable to one of the
applyOn
exceptions, fallback is applied; -
otherwise, if the cause chain of the exception contains an exception assignable to one of the
skipOn
exceptions, fallback is skipped and the exception is rethrown; -
otherwise, if the cause chain of the exception contains an exception assignable to one of the
applyOn
exceptions, fallback is applied; -
otherwise, the exception is rethrown.
For example, say we have this method:
@Fallback(fallbackMethod = "fallback",
skipOn = ExpectedOutcomeException.class,
applyOn = IOException.class)
public Result doSomething() {
...
}
public Result fallback() {
...
}
If doSomething
throws an ExpectedOutcomeException
, fallback is skipped and the exception is thrown.
If doSomething
throws an IOException
, fallback is applied.
If doSomething
throws a WrapperException
whose cause is ExpectedOutcomeException
, fallback is skipped and the exception is thrown.
If doSomething
throws a WrapperException
whose cause is IOException
, fallback is applied.
Comparing with the @Fallback
specification, SmallRye Fault Tolerance inserts 2 more steps into the decision process that inspect the cause chain.
Note that these steps are executed if and only if the thrown exception matches neither skipOn
nor applyOn
.
If the thrown exception matches either of them, the cause chain is not inspected at all.
Similar behavior applies to @CircuitBreaker
and @Retry
.
All 3 annotations follow the same principle: exceptions considered success have priority over those considered failure.
Fault Tolerance annotation | Exception is first tested against | and then against |
---|---|---|
|
|
|
|
|
|
|
|
|
Kotlin suspend
Functions
SmallRye Fault Tolerance includes support for Kotlin suspending functions. They are treated as Additional Asynchronous Types, even though the internal implementation is more complex than support for Mutiny or RxJava.
For example:
@ApplicationScoped
open class MyService {
@Retry(maxRetries = 2)
@Fallback(fallbackMethod = "helloFallback")
open suspend fun hello(): String { (1)
delay(100)
throw IllegalArgumentException()
}
private suspend fun helloFallback(): String { (2)
delay(100)
return "hello"
}
}
1 | As a suspending function, this method can only be called from another suspending function. It will be guarded by the retry and fallback strategies, as defined using the annotations. |
2 | Similarly to fallback methods in Java, fallback methods in Kotlin must have the same signature as the guarded method. Since the guarded method is suspending, the fallback method must be suspending. |
As mentioned above, suspending functions are treated as async types.
This means that for asynchronous fault tolerance to work correctly on suspending functions, they must be determined to be asynchronous.
That happens automatically in the non-compatible mode, based on the method signature, but if you use strictly compatible mode, one of the usual annotations (@Blocking
, @NonBlocking
, @Asynchronous
) must be present.
It is expected that most users will use the Kotlin support in the non-compatible mode, so the example above does not include any such annotation.
To be able to use this, a support library must be present.
It is possible that the runtime you use already provides the correct integration.
Otherwise, add a dependency to your application: io.smallrye:smallrye-fault-tolerance-kotlin
.
Programmatic API
Suspending functions are currently only supported in the declarative, annotation-based API, as shown in the example above. The Programmatic API of SmallRye Fault Tolerance does not support suspending functions, but other than that, it can of course be used from Kotlin through its Java interop.
Reusable, Preconfigured Fault Tolerance
The declarative, annotation-based API of MicroProfile Fault Tolerance doesn’t allow sharing configuration of fault tolerance strategies across multiple classes. In a single class, the configuration may be shared across all methods by putting the annotations on the class instead of individual methods, but even then, stateful fault tolerance strategies are not shared. Each method has its own bulkhead and/or circuit breaker, which is often not what you want.
The programmatic API of SmallRye Fault Tolerance allows using a single FaultTolerance
object to guard multiple disparate actions, which allows reuse and state sharing.
It is possible to use a programmatically constructed FaultTolerance
object declaratively, using the @ApplyFaultTolerance
annotation.
To be able to do that, we need a bean of type FaultTolerance
with the @Identifier
qualifier:
@ApplicationScoped
public class PreconfiguredFaultTolerance {
@Produces
@Identifier("my-fault-tolerance")
public static final FaultTolerance<String> FT = FaultTolerance.<String>create()
.withRetry().maxRetries(2).done()
.withFallback().handler(() -> "fallback").done()
.build();
}
See the programmatic API documentation for more information about creating the FaultTolerance
instance.
It is customary to create the bean by declaring a static
producer field, just like in the previous example.
Once we have that, we can apply my-fault-tolerance
to synchronous methods that return String
:
@ApplicationScoped
public class MyService {
@ApplyFaultTolerance("my-fault-tolerance")
public String doSomething() {
...
}
}
It is also possible to create a bean of type FaultTolerance<Object>
and apply it to synchronous methods that return many different types.
Note that this effectively precludes defining a useful fallback, because fallback can only be defined when the value type is known.
It is also possible to define a bean of type FaultTolerance<CompletionStage<T>>
and apply it to asynchronous methods that return CompletionStage<T>
.
Likewise, it is possible to do this for Additional Asynchronous Types.
Note that you can’t define a synchronous FaultTolerance<T>
object and apply it to any asynchronous method.
Similarly, you can’t define an asynchronous FaultTolerance<CompletionStage<T>>
and apply it to a synchronous method or an asynchronous method with different asynchronous type.
This limitation will be lifted in the future.