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.
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:
-
be annotated with
@AutoConfig
; -
extend the annotation interface (
Fallback
in this case); -
extend the
Config
interface (from theautoconfig/core
module); -
provide a
default
implementation of thevalidate
method (defined inConfig
).
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.