Programmatic API
This is an additional feature of SmallRye Fault Tolerance and is not specified by MicroProfile Fault Tolerance. |
In addition to the declarative, annotation-based API of MicroProfile Fault Tolerance, SmallRye Fault Tolerance also offers a programmatic API for advanced scenarios.
This API is present in the io.smallrye:smallrye-fault-tolerance-api
artifact, just like all other additional APIs SmallRye Fault Tolerance provides.
Installation
If you use SmallRye Fault Tolerance as part of a runtime that implements MicroProfile Fault Tolerance, you don’t have to do anything. The programmatic API is ready to use. In this documentation, we’ll call this the CDI implementation of the programmatic API, because it’s integrated with the MicroProfile Fault Tolerance implementation, which is naturally based on CDI.
In addition to the CDI implementation, SmallRye Fault Tolerance also offers a standalone implementation that is meant to be used outside of any runtime.
This implementation does not need CDI or anything else.
If you want to use SmallRye Fault Tolerance in a standalone fashion, just add a dependency on io.smallrye:smallrye-fault-tolerance-standalone
.
The API is brought in transitively.
Usage
The entrypoints to the programmatic API are the Guard
and TypedGuard
interfaces.
These interfaces represent a configured set of fault tolerance strategies. Their configuration, order of application and behavior in general corresponds to the declarative API, so if you know that, you’ll feel right at home. If not, the javadoc has all the information you need (though it often points to the annotation-based API for more information).
The interfaces are very similar, there are only 2 differences:
-
Guard
requires specifying the return type of the guarded operation for eachcall()
orget()
(oradapt*
) method call.TypedGuard
, on the other hand, requires specifying the return type once, when creating an instance usingcreate()
. Therefore,Guard
may be used for guarding many different types, whileTypedGuard
may only be used to guard single type. -
TypedGuard
allows defining a fallback.Guard
does not.
In the following text, we’ll only talk about Guard
, but it also applies to TypedGuard
without a change (except of the differences mentioned above).
To create an instance of Guard
, you can use the create
static method.
It returns a builder which has to be used to add and configure all the fault tolerance strategies that should apply.
There is no external configuration, so all configuration properties have to be set explicitly, using the builder methods.
If you don’t set a configuration property, it will default to the same value the annotation-based API uses.
Let’s take a look at a simple example:
public class MyService {
private static final Guard GUARD = Guard.create()
.withRetry().maxRetries(3).done()
.build();
public String hello() throws Exception {
return GUARD.call(() -> externalService.hello(), String.class); (1)
}
}
1 | Here, we call externalService.hello() and guard the call with the previously configured set of fault tolerance strategies.
The call method used here takes the Callable type, which represent the guarded action.
The get method works just like call , but accepts a Supplier . |
The previous example shows how to apply fault tolerance to synchronous actions.
SmallRye Fault Tolerance naturally also supports guarding asynchronous actions, using the CompletionStage
type.
Unlike the declarative API, the programmatic API doesn’t support asynchronous actions that return the Future
type.
public class MyService {
private static final Guard GUARD = Guard.create()
.withBulkhead().done()
.withThreadOffload(true) (1)
.build();
public CompletionStage<String> hello() throws Exception {
return GUARD.call(() -> externalService.hello(),
new TypeLiteral<CompletionStage<String>>() {}); (2)
}
}
1 | The thread offload here only applies to asynchronous actions.
If you use the same GUARD to guard a synchronous action, no thread offload will apply. |
2 | Note that here, we use the TypeLiteral class (from CDI) to specify the type of the guarded action. |
Asynchronous actions may be blocking or non-blocking.
In the example above, we assume the externalService.hello()
call is blocking, so we set thread offload to true
.
SmallRye Fault Tolerance will automatically move the actual execution of the action to another thread.
If we didn’t configure withThreadOffload
, however, the execution of an asynchronous action would continue on the original thread.
This is often desired for non-blocking actions, which are very common in modern reactive architectures.
Also note that in this example, we configured multiple fault tolerance strategies: bulkhead and thread offload.
When that happens, the fault tolerance strategies are ordered according to the MicroProfile Fault Tolerance specification, just like in the declarative API.
Order of all the with*
method invocations doesn’t matter.
Single-Action Usage
The Guard
API is general and permits guarding multiple different actions using the same set of fault tolerance strategies.
Often, we need to guard just a single action, although possibly several times.
For such use case, the Guard
instance may be adapted to a Callable<T>
or Supplier<T>
using the adapt*
methods:
public class MyService {
private static final Callable<String> guard = Guard.create()
.withTimeout().duration(5, ChronoUnit.SECONDS).done()
.build() (1)
.adaptCallable(() -> externalService.hello(), String.class); (2)
public String hello() throws Exception {
return callable.call(); (3)
}
}
1 | Create a Guard object that can guard arbitrary actions. |
2 | Adapt the general Guard instance to a Callable that guards the externalService.hello() invocation.
Similar method exists that accepts and returns a Supplier : adaptSupplier . |
3 | You can do whatever you wish with the adapted Callable .
Here, we just call it once, which isn’t very interesting, but it could possibly be called multiple times, passed to other methods etc. |
Synchronous vs. Asynchronous
The Guard
and TypedGuard
interfaces both decide whether the guarded action is synchronous or asynchronous based on the action type.
This is given to Guard
when calling or adapting the action, and to TypedGuard
when creating it.
If the type is asynchronous (such as CompletionStage
), the asynchronous behavior will also be guarded; the action is only considered complete when the CompletionStage
actually completes.
If the type is not asynchronous, only the synchronous behavior will be guarded; the action is considered complete when the method returns.
Mutiny Support
It is enough to include the Mutiny support library io.smallrye:smallrye-fault-tolerance-mutiny
, as described in Additional Asynchronous Types.
With this library present, both Guard
and TypedGuard
will recognize Uni
as an asynchronous type and guard it properly.
Guarding a Multi
is not supported.
For example:
public class MyService {
private final Supplier<Uni<String>> GUARD = TypedGuard.create(
new TypeLiteral<Uni<String>>() {})
.withTimeout().duration(5, ChronoUnit.SECONDS).done()
.withFallback().handler(() -> Uni.createFrom().item("fallback")).done()
.build()
.adaptSupplier(() -> externalService.hello()); (1)
public Uni<String> hello() {
return guard.get();
}
}
1 | The call to externalService.hello() is supposed to return Uni<String> . |
Note that the Uni
type is lazy, so the action itself won’t execute until the guarded Uni
is subscribed to.
Stateful Fault Tolerance Strategies
The bulkhead, circuit breaker and rate limit strategies are stateful. That is, they hold some state required for their correct functioning, such as the number of current executions for bulkhead, the rolling window of successes/failures for circuit breaker, or the time window for rate limit. If you use these strategies, you have to consider their lifecycle.
The SmallRye Fault Tolerance programmatic API makes such reasoning pretty straightforward.
Each Guard
object has its own instance of each fault tolerance strategy, including the stateful strategies.
If you use a single Guard
object for guarding multiple different actions, all those actions will be guarded by the same bulkhead, circuit breaker and/or rate limit.
If, on the other hand, you use different Guard
objects for guarding different actions, each action will be guarded by its own bulkhead, circuit breaker and/or rate limit.
If you call the adapt*
methods on the same Guard
multiple times, the resulting Callable
or Supplier
objects will guard the underlying action using the original Guard
instance, so stateful strategies will be shared.
Circuit Breaker Maintenance
The CircuitBreakerMaintenance
API, accessed through CircuitBreakerMaintenance.get()
or by injection in the CDI implementation, can be used to manipulate all named circuit breakers.
A circuit breaker is given a name by calling withCircuitBreaker().name("...")
on the fault tolerance builder, or using the @CircuitBreakerName
annotation in the declarative API.
Additionally, CircuitBreakerMaintenance.resetAll()
will also reset all unnamed circuit breakers declared using the @CicruitBreaker
annotation.
For this to work, all unnamed circuit breakers have to be remembered.
This is safe in case of the declarative, annotation-based API, because the number of such declared circuit breakers is fixed.
At the same time, this would not be safe to do for all unnamed circuit breakers created using the programmatic API, as their number is potentially unbounded.
(In other words, remembering all unnamed circuit breakers created using the programmatic API would easily lead to a memory leak.)
Therefore, all circuit breakers created using the programmatic API must be given a name when CircuitBreakerMaintenance
is supposed to affect them.
Note that duplicate names are not permitted and lead to an error, so lifecycle of the circuit breaker must be carefully considered.
Event Listeners
The programmatic API has one feature that the declarative API doesn’t have: ability to observe certain events. For example, when configuring a circuit breaker, it is possible to register a callback for circuit breaker state changes or for a situation when an open circuit breaker prevents an invocation. When configuring a timeout, it is possible to register a callback for when the invocation times out, etc. etc. For example:
private static final Guard GUARD = Guard.create()
.withTimeout().duration(5, ChronoUnit.SECONDS).onTimeout(() -> ...).done() (1)
.build();
1 | The onTimeout method takes a Runnable that will later be executed whenever an invocation guarded by GUARD times out. |
All event listeners registered like this must run quickly and must not throw exceptions.
Configuration
As mentioned above, except of smallrye.faulttolerance.enabled
/ MP_Fault_Tolerance_NonFallback_Enabled
, there is no support for external configuration of fault tolerance strategies.
This may change in the future, though possibly only in the CDI implementation.
Metrics
The programmatic API is integrated with metrics.
All metrics, as described in the Metrics reference guide and the linked guides, are supported.
The only difference is the value of the method
tag.
With the programmatic API, the method
tag will be set to the description of the guarded operation, provided on the Guard
builder.
private static final Guard GUARD = Guard.create()
.withDescription("hello") (1)
.withRetry().maxRetries(3).done()
.build();
1 | A description of hello is set, it will be used as a value of the method tag in all metrics. |
It is possible to create multiple Guard
objects with the same description.
In this case, it won’t be possible to distinguish the different Guard
objects in metrics; their values will be aggregated.
If no description is provided, a random UUID is used.
Differences to the Specification
Guard
and TypedGuard
have the following differences to standard MicroProfile Fault Tolerance:
-
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
The Guard
and TypedGuard
APIs do not support Kotlin suspend
functions at the moment.
Integration Concerns
Integration concerns, which are particularly interesting for users of the standalone implementation, are described in the integration section.
Migration from FaultTolerance
The 1st version of the programmatic API had the FaultTolerance
interface.
This is deprecated and scheduled for removal in SmallRye Fault Tolerance 7.0.
The replacement are the Guard
and TypedGuard
types.
When migrating, one first has to decide which type to use as a replacement.
The TypedGuard
is much closer to the FaultTolerance
type, so that is the easiest way to migrate.
There are some differences still:
-
the
create()
static method now takes a parameter that expresses the guarded action type, either as aClass
or as aTypeLiteral
; -
there are no
cast()
andcastAsync()
methods, becauseTypedGuard
only guards actions of a single type; -
there is no support for guarding or adapting
Runnable
s, onlyCallable
s andSupplier
s are supported. As a replacement forRunnable
, aSupplier<Void>
(orSupplier<CompletionStage<Void>>
etc.) can be used.
For example, these usages of FaultTolerance
:
static final FaultTolerance<String> FT1 = FaultTolerance.<String>create()
.withDescription("ft1")
.withRetry().maxRetries(3).done()
.withFallback().handler(() -> "fallback").done()
.build();
static final FaultTolerance<CompletionStage<String>> FT2 = FaultTolerance.<String>createAsync()
.withDescription("ft2")
.withBulkhead().limit(5).queueSize(100).done()
.withTimeout().duration(3, ChronoUnit.SECONDS).done()
.build();
can be rewritten to:
static final TypedGuard<String> GUARD1 = TypedGuard.create(String.class)
.withDescription("ft1")
.withRetry().maxRetries(3).done()
.withFallback().handler(() -> "fallback").done()
.build();
static final TypedGuard<CompletionStage<String>> GUARD2 = TypedGuard.create(
new TypeLiteral<CompletionStage<String>>() {})
.withDescription("ft2")
.withBulkhead().limit(5).queueSize(100).done()
.withTimeout().duration(3, ChronoUnit.SECONDS).done()
.build();
After creating a TypedGuard
, there’s no change in how it’s used.
The methods call()
, get()
, adaptCallable()
and adaptSupplier()
have the same signatures as before.
TypedGuard
is the only possible replacement if you need fallback.
If you don’t need fallback, and especially if you need to guard actions of multiple types, Guard
is a better choice.
The differences are:
-
Guard
has no type parameter (is not generic) and does not allow defining fallback; -
there are no
cast()
andcastAsync()
methods, because they are no longer needed; -
there is no support for guarding or adapting
Runnable
s, onlyCallable
s andSupplier
s are supported. As a replacement forRunnable
, aSupplier<Void>
(orSupplier<CompletionStage<Void>>
etc.) can be used.
For example, these usages of FaultTolerance
:
static final FaultTolerance<String> FT1 = FaultTolerance.<String>create()
.withDescription("ft1")
.withRetry().maxRetries(3).done()
.withCircuitBreaker().done()
.build();
static final FaultTolerance<CompletionStage<String>> FT2 = FaultTolerance.<String>createAsync()
.withDescription("ft2")
.withBulkhead().limit(5).queueSize(100).done()
.withTimeout().duration(3, ChronoUnit.SECONDS).done()
.build();
can be rewritten to:
static final Guard GUARD1 = Guard.create()
.withDescription("ft1")
.withRetry().maxRetries(3).done()
.withCircuitBreaker().done()
.build();
static final Guard GUARD2 = Guard.create()
.withDescription("ft2")
.withBulkhead().limit(5).queueSize(100).done()
.withTimeout().duration(3, ChronoUnit.SECONDS).done()
.build();
Now, as mentioned above, Guard
is used slightly differently than FaultTolerance
.
The methods call()
, get()
, adaptCallable()
and adaptSupplier()
take an extra parameter that expresses the type of the guarded action, again either as a Class
or as a TypeLiteral
.
For example, these usages of FaultTolerance
:
String result1 = FT1.call(() -> externalService.hello());
CompletionStage<String> result2 = FT2.get(() -> externalService.helloAsync());
Callable<String> callable = FT1.adaptCallable(
() -> externalService.hello());
Supplier<CompletionStage<String>> supplier = FT2.adaptSupplier(
() -> externalService.helloAsync());
can be rewritten to:
String result1 = GUARD1.call(() -> externalService.hello(), String.class);
CompletionStage<String> result2 = GUARD2.get(() -> externalService.helloAsync(),
new TypeLiteral<CompletionStage<String>>() {});
Callable<String> callable = GUARD1.adaptCallable(
() -> externalService.hello(), String.class);
Supplier<CompletionStage<String>> supplier = GUARD2.adaptSupplier(
() -> externalService.helloAsync(),
new TypeLiteral<CompletionStage<String>>() {});