Reusable Fault Tolerance
This is an additional feature of SmallRye Fault Tolerance and is not specified by MicroProfile 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, circuit breaker and/or rate limit, which is often not what you want.
The programmatic API of SmallRye Fault Tolerance allows using a single Guard
or TypedGuard
object to guard multiple disparate actions, which allows reuse and state sharing.
It is possible to use a programmatically constructed Guard
or TypedGuard
object declaratively, using the @ApplyGuard
annotation.
To be able to do that, we need a bean of type Guard
(or TypedGuard
) with the @Identifier
qualifier:
@ApplicationScoped
public class PreconfiguredFaultTolerance {
@Produces
@Identifier("my-fault-tolerance")
public static final Guard GUARD = Guard.create()
.withRetry().maxRetries(2).done()
.withTimeout().done()
.build();
}
See the programmatic API documentation for more information about creating the Guard
or TypedGuard
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 any method:
@ApplicationScoped
public class MyService {
@ApplyGuard("my-fault-tolerance")
public String doSomething() {
...
}
@ApplyGuard("my-fault-tolerance")
public CompletionStage<Integer> doSomethingElse() {
...
}
}
Defining Fallback
Note that it is not possible to define a fallback on Guard
, because fallback is tied to the action type.
It is possible to define a fallback on TypedGuard
, because it can only be used to guard methods with a single return type, equal to the type the TypedGuard
was created with.
However, the @ApplyGuard
annotation pays attention to the @Fallback
annotation.
If @Fallback
is defined, it is used both by Guard
and TypedGuard
instances, and it overrides the possible fallback defined on the TypedGuard
.
Defining Thread Offload
Both Guard
and TypedGuard
allow enabling or disabling thread offload.
This is ignored by @ApplyGuard
if the annotated method is asynchronous as determined from the method signature and possible annotations (@Asynchronous
and @AsynchronousNonBlocking
).
See Asynchronous Execution for more information about how that determination works.
The thread offload configuration of Guard
or TypedGuard
is only honored when the method cannot be determined to be asynchronous, but it still declares an asynchronous return type.
Configuration
Even though the programmatic API of Guard
and TypedGuard
does not support configuration, the declarative API of @ApplyGuard
does.
The configuration closely resembles the configuration of the declarative API, where instead of the <classname>
or <classname>/<methodname>
, you use the identifier that’s used in the @Identifier
qualifier and in the @ApplyGuard
annotation.
When configuring the Guard or TypedGuard using the following configuration properties, it is recommended to never use the [Typed]Guard programmatically.
The reason is that configuration is applied on the first invocation, and if that invocation is programmatic, the configuration key is unknown.
|
For example, let’s assume the following preconfigured Guard
object:
@ApplicationScoped
public class PreconfiguredFaultTolerance {
@Produces
@Identifier("my-fault-tolerance")
public static final Guard GUARD = Guard.create()
.withRetry().maxRetries(2).done()
.withTimeout().done()
.build();
}
This Guard
supports retry and timeout; any attempt to configure other strategies will be ignored.
To reconfigure the number of maximum retries, the following configuration properties may be used:
smallrye.faulttolerance."my-fault-tolerance".retry.max-retries=5
# alternatively, a specification-defined property can be used
my-fault-tolerance/Retry/maxRetries=5
Global configuration that applies to all usages of retry is also respected by @ApplyGuard
:
smallrye.faulttolerance.global.retry.max-retries=5
# alternatively, a specification-defined property can be used
Retry/maxRetries=5
Note that the configuration keys follow the configuration of the declarative API, even if the methods in the programmatic API builders are named differently.
For example, to set the rate limit in the programmatic API, the RateLimitBuilder
uses a method called limit()
, while the corresponding annotation member of @RateLimit
is named value()
.
The configuration key in both cases is rate-limit.value
(or RateLimit/value
).
Disabling Fault Tolerance
Configuration properties for disabling individual strategies also work:
smallrye.faulttolerance."my-fault-tolerance".retry.enabled=false
# alternatively, a specification-defined property can be used
my-fault-tolerance/Retry/enabled=false
Or globally:
smallrye.faulttolerance.global.retry.enabled=false
# alternatively, a specification-defined property can be used
Retry/enabled=false
Supported Strategies
The following fault tolerance strategies, when defined on a Guard
(or TypedGuard
) and used through @ApplyGuard
, may be configured:
-
bulkhead (except of
BulkheadBuilder.enableSynchronousQueueing()
) -
circuit breaker
-
rate limit
-
retry, including exponential backoff and Fibonacci backoff
-
timeout
In the programmatic API, the following strategies are not built declaratively, and so their configuration is ignored:
-
fallback
-
circuit breaker name (cannot be configured in the declarative API either)
-
certain parts of retry: custom backoff, before retry action and retry predicates
-
thread offload
Metrics
Methods annotated @ApplyGuard
gather metrics similarly to methods annotated with MicroProfile Fault Tolerance annotations.
That is, each method gets its own metrics, with the method
tag being <fully qualified class name>.<method name>
.
At the same time, state is still shared.
All methods annotated @ApplyGuard
share the same bulkhead, circuit breaker and/or rate limit.
If the Guard
or TypedGuard
object used for @ApplyGuard
is also used programmatically, that usage is coalesced in metrics (where the method
tag is set to the description).
Differences to the Specification
@ApplyGuard
has the same differences to standard MicroProfile Fault Tolerance as Guard
/ TypedGuard
:
-
asynchronous actions of type
java.util.concurrent.Future
are not supported; -
the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode.
Kotlin suspend
Functions
Even though the programmatic API of Guard
and TypedGuard
does not support Kotlin suspend
functions, the declarative API of @ApplyGuard
does.
When the guard is a Guard
, no restrictions apply.
When the guard is a TypedGuard
, however, its type must be a synchronous return type of the suspend
function.
For example, when the suspend
function is declared to return a String
asynchronously:
@ApplyGuard("my-fault-tolerance")
@Fallback(fallbackMethod = "fallback")
suspend fun hello(): String {
delay(100)
throw IllegalArgumentException()
}
The TypedGuard
must be declared to guard actions of type String
:
@Produces
@Identifier("my-fault-tolerance")
val GUARD = TypedGuard.create(String::class.java)
.withRetry().maxRetries(2).done()
.withFallback().handler(Supplier { "fallback" }).done()
.build()
This means that a possible fallback declared on the TypedGuard
must be synchronous; it cannot be a suspend
lambda.
The @Fallback
method, if declared, must have a matching signature and so must be a suspend
function:
suspend fun fallback(): String {
delay(100)
return "fallback"
}
Migration from @ApplyFaultTolerance
The 1st version of the programmatic API had the @ApplyFaultTolerance
annotation.
That annotation is deprecated and scheduled for removal in SmallRye Fault Tolerance 7.0.
To migrate, replace @ApplyFaultTolerance
with @ApplyGuard
and change the FaultTolerance<>
producers to produce Guard
or TypedGuard<>
.
See the programmatic API migration guide for more details about that.
Note that it is not possible to define both Guard
and TypedGuard<>
with the same identifier; that leads to a deployment problem.
Therefore, for each producer of FaultTolerance<>
, you have to decide whether the replacement should be Guard
or TypedGuard<>
.