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.

Quarkus

In Quarkus, the SmallRye Fault Tolerance API is brought in automatically, as a transitive dependency of the Quarkus extension for SmallRye Fault Tolerance. That is, you don’t need to do anything to be able to use the programmatic API.

WildFly

In WildFly, the SmallRye Fault Tolerance API is not readily available to deployments. If you want to use it, you need to add a module dependency to the deployment using jboss-deployment-structure.xml.

Note that at the time of this writing, the SmallRye Fault Tolerance module in WildFly is considered private. If you do add a module dependency on it, to be able to use the SmallRye Fault Tolerance API, you may be stepping out of the WildFly support scope.

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:

  1. Guard requires specifying the return type of the guarded operation for each call() or get() (or adapt*) method call. TypedGuard, on the other hand, requires specifying the return type once, when creating an instance using create(). Therefore, Guard may be used for guarding many different types, while TypedGuard may only be used to guard single type.

  2. 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.

Disabling Fault Tolerance

There’s one exception to the "no external configuration" rule.

The CDI implementation looks for the smallrye.faulttolerance.enabled / MP_Fault_Tolerance_NonFallback_Enabled configuration properties in MicroProfile Config. The standalone implementation looks for system properties with the same name, or obtains the value from custom configuration (as described in the integration section).

If at least one of these properties exists and is set to false, only the fallback and thread offload fault tolerance strategies will be applied. Everything else will be ignored.

Note that this is somewhat different to the declarative, annotation-based API, where only fallback is retained and the @Asynchronous strategy is skipped as well. Since this significantly changes execution semantics, the programmatic API will apply thread offload even if fault tolerance is disabled.

Similarly to the declarative API, implementations of the programmatic API also read this property only once, when the Guard API is first used. It is not read again later.

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.

Quarkus

In Quarkus, the Mutiny support library is present by default. You can guard Uni-returning actions out of the box.

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 a Class or as a TypeLiteral;

  • there are no cast() and castAsync() methods, because TypedGuard only guards actions of a single type;

  • there is no support for guarding or adapting Runnables, only Callables and Suppliers are supported. As a replacement for Runnable, a Supplier<Void> (or Supplier<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() and castAsync() methods, because they are no longer needed;

  • there is no support for guarding or adapting Runnables, only Callables and Suppliers are supported. As a replacement for Runnable, a Supplier<Void> (or Supplier<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>>() {});