Fault Tolerance Core

Note that everything explained here is an implementation detail. API stability for these interfaces and classes is not guaranteed! This documentation is only present to provide implementation insight for people who have to debug issues or want to contribute.

The core idea in SmallRye Fault Tolerance is a FaultToleranceStrategy. It is an interface that looks like this:

interface FaultToleranceStrategy<V> {
    Future<V> apply(FaultToleranceContext<V> ctx);
}
The Future type here is not a java.util.concurrent.Future. It comes from SmallRye Fault Tolerance and it could be described as a very bare-bones variant of CompletableFuture, except it’s split into Completer and Future. It is not supposed to be used outside of this project.

The FaultToleranceContext is similar to a Callable<Future<V>>; it represents the method invocation guarded by this fault tolerance strategy. The fault tolerance strategy does its work around ctx.call(). It can catch exceptions, invoke ctx.call() multiple times, invoke something else, etc. As an example, let’s consider this strategy, applicable to methods that return a String:

public class MyStringFallback implements FaultToleranceStrategy<String> {
    @Override
    public Future<String> apply(FaultToleranceContext<String> ctx) {
        Completer<String> completer = Completer.create();
        try {
            ctx.call().then((value, error) -> {
                if (error == null) {
                    completer.complete(value);
                } else {
                    completer.complete("my string value");
                }
            });
        } catch (Exception ignored) {
            completer.complete("my string value");
        }
        return completer.future();
    }
}

This is a very simple fallback mechanism, which returns a pre-defined value in case of an exception.

In the SmallRye Fault Tolerance codebase, you can find implementations of all the strategies required by MicroProfile Fault Tolerance: retry, fallback, timeout, circuit breaker or bulkhead. Asynchronous invocation, delegated to a thread pool, is of course also supported.

When multiple fault tolerance strategies are supposed to be used to guard one method, they form a chain. Continuing with our simple example, adding the ability to chain with another strategy would look like this:

public class MyStringFallback implements FaultToleranceStrategy<String> {
    private final FaultToleranceStrategy<String> delegate;

    public MyStringFallback(FaultToleranceStrategy<String> delegate) {
        this.delegate = delegate;
    }

    @Override
    public Future<String> apply(FaultToleranceContext<String> ctx) {
        Completer<String> completer = Completer.create();
        try {
            delegate.apply(ctx).then((value, error) -> {
                if (error == null) {
                    completer.complete(value);
                } else {
                    completer.complete("my string value");
                }
            });
        } catch (Exception ignored) {
            completer.complete("my string value");
        }
        return completer.future();
    }
}

We see that one strategy delegates to another, passing the FaultToleranceContext along. In fact, all the implementations in SmallRye Fault Tolerance are written like this: they expect to be used in a chain, so they take another FaultToleranceStrategy to which they delegate. But if all strategies have this form, when is ctx.call() actually invoked? Good question! The ultimate ctx.call() invocation is done by a special fault tolerance strategy which is called, well, Invocation.

As an example which uses real MicroProfile Fault Tolerance annotations, let’s consider this method:

@Retry(...)
@Timeout(...)
@Fallback(...)
public void doSomething() {
    ...
}

The chain of fault tolerance strategies will look roughly like this:

Fallback(
    Retry(
        Timeout(
            Invocation(
                // ctx.call() will happen here
                // that will, in turn, invoke doSomething()
            )
        )
    )
)

The order in which the strategies are chained (or, in fact, nested) is specified by MicroProfile Fault Tolerance.

Other strategies might also be present in more complex cases. For example, if metrics are enabled, a special strategy is used that collects metrics. If the method is @Asynchronous, a special strategy is used that offloads method execution to another thread. Etc.