Configuration System

Please 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 fault tolerance strategies are configured simply by constructor parameters. This is fine, since the they are an internal implementation detail. However, the constructor parameters correspond closely to the members of MicroProfile Fault Tolerance annotations. The idea is that core fault tolerance strategies have no dependency on the MicroProfile Fault Tolerance API, but given a fault tolerance annotation, creating an instance of the corresponding strategy should be straightforward.

On top of the core layer, SmallRye Fault Tolerance implements MicroProfile Fault Tolerance, including a configuration system conforming to the specification. This document briefly describes how the configuration system works.

FaultToleranceOperation

The fault tolerance interceptor, which is the main entrypoint, looks up a FaultToleranceOperation instance for each class/method combination that it has to process. The FaultToleranceOperation holds a bean class, a descriptor of the guarded method, and configuration for each fault tolerance strategy applied to the method. It also orchestrates configuration validation. The interceptor uses a FaultToleranceOperation to instantiate all the core fault tolerance strategies for given guarded method.

The FaultToleranceOperation instances are created early on, during application deployment, for all beans that use MicroProfile Fault Tolerance. It may happen that a FaultToleranceOperation is created lazily, for special constructs such as runtime-built proxies, but that is largely irrelevant from the configuration perspective.

FaultToleranceMethod

A FaultToleranceOperation is created from a FaultToleranceMethod. A FaultToleranceMethod holds, again, a bean class, a descriptor of the guarded method, and the annotation instance for each annotation that is present on the guarded method or the class declaring the method. It also knows what annotations have been declared directly on the method, and what annotations have been declared on the class. This is because the MicroProfile Fault Tolerance configuration rules distinguish these two cases.

A FaultToleranceMethod must be created in a CDI extension, because it represents the knowledge CDI has about the guarded method. Note that CDI extensions may add, remove or modify annotations, so FaultToleranceMethod doesn’t just hold the annotation instances as present in the class bytecode.

Quarkus

In environments that bootstrap the CDI container during application build, such as Quarkus, the set of all FaultToleranceMethods in the application should be collected during build and transferred to runtime. Then, at application runtime, FaultToleranceOperations should be created and validated.

SmallRye Fault Tolerance doesn’t do anything like that, but is designed with Quarkus (and eventual CDI Lite) in mind.

The main difference between FaultToleranceMethod and FaultToleranceOperation is that FaultToleranceMethod holds annotation instances, while FaultToleranceOperation holds configuration.

@AutoConfig

As described above, FaultToleranceMethod holds annotation instances, while FaultToleranceOperation holds configuration. Yet, FaultToleranceOperation exposes the annotation types (such as @Fallback) to anyone who needs the configuration. The configuration consumers simply call the annotation methods (such as fallbackMethod()), and configuration is handled behind the scenes, following the MicroProfile Fault Tolerance configuration rules.

The infrastructure behind this is present in the autoconfig module. This module allows creating simple configuration interfaces like this:

@AutoConfig
interface FallbackConfig extends Fallback, Config {
    default void validate() {
        ...
    }
}

The config interface must:

  1. be annotated with @AutoConfig;

  2. extend the annotation interface (Fallback in this case);

  3. extend the Config interface (from the autoconfig/core module);

  4. provide a default implementation of the validate method (defined in Config).

For each such interface, an implementation class will be automatically generated by an annotation processor (implemented in the autoconfig/processor module). In this case, it would be called FallbackConfigImpl.

This implementation class has a static method called create that accepts a FaultToleranceMethod. It follows that for each guarded method, a new instance must be created. If given FaultToleranceMethod doesn’t contain an instance of particular annotation, create simply returns null.

The implementation class also overrides all methods from the annotation type. Each annotation method is implemented to first consult MicroProfile Config, and only second to consult the annotation instance (which, as we described above, is present on the FaultToleranceMethod).

The implementation class is supposed to be used just like the annotation type itself. In other words, to obtain configured values, call the annotation methods on the config interface instance. For example, to obtain the fallbackMethod value, with respect to the MicroProfile Fault Tolerance configuration rules, we obtain an instance of FallbackConfigImpl and call fallbackMethod.

The FaultToleranceOperation class holds instances of these config interfaces, and exposes them to configuration consumers. It actually only exposes the annotation types, not the full config interface, but that is enough.

Putting it all together

Let’s recap how it all works.

First, for each guarded method in the application, a FaultToleranceMethod instance is created, holding all the annotations that affect the method.

Second, for each FaultToleranceMethod, a FaultToleranceOperation is created. That, in turn, creates an instance of all relevant config interfaces.

Finally, the fault tolerance interceptor looks up a FaultToleranceOperation and creates a chain of fault tolerance strategies. For each fault tolerance strategy in the chain, the interceptor looks up configuration values from the corresponding config interface.

The chain of fault tolerance strategies is created on the first invocation to a given guarded method and cached for all subsequent invocations. That is how we satisfy the requirement that all stateful fault tolerance strategies are singletons, but that’s a different story.