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.

Compatible Mode

In the compatible mode, methods are determined to be asynchronous when they are annotated @Asynchronous or @AsynchronousNonBlocking and they declare an asynchronous return type. For such methods, the Guard / TypedGuard thread offload configuration does not apply.

For methods that declare an asynchronous return type but are not annotated with @Asynchronous or @AsynchronousNonBlocking, the configuration on Guard / TypedGuard is honored.

Non-compatible Mode

In the non-compatible mode, methods are determined to be asynchronous whenever they declare an asynchronous return type. The @Asynchronous and @AsynchronousNonBlocking only affect whether the invocation of that method is offloaded to an extra thread. If none of these annotations is present, no thread offload happens.

In other words, in non-compatible mode, the Guard / TypedGuard thread offload configuration never applies.

See Non-compatible Mode for more information about the non-compatible mode.

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 under the description as the method tag.

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