Configuration System
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 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 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 @CircuitBreaker
) to anyone who needs the configuration.
The configuration consumers simply call the annotation methods (such as requestVolumeThreshold()
), 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 CircuitBreakerConfig extends CircuitBreaker, Config {
default void validate() {
...
}
}
The config interface must:
-
be annotated with
@AutoConfig
; -
extend the annotation interface (
CircuitBreaker
in this case); -
extend the
Config
(orConfigDeclarativeOnly
) 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 CircuitBreakerConfigImpl
.
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 requestVolumeThreshold
value, with respect to the MicroProfile Fault Tolerance configuration rules, we obtain an instance of CircuitBreakerConfigImpl
and call requestVolumeThreshold()
.
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.
Supplement: Programmatic API
The description above is slightly simplified for readability. It is precise enough to describe how configuration works in the declarative, annotation-based API.
The programmatic API itself supports configuration when used together with @ApplyGuard
.
However, it doesn’t support configuring all strategies; some are built in a different manner and no configuration is possible.
Therefore, the FaultToleranceOperation
class described above is actually split into 2 classes:
-
BasicFaultToleranceOperation
, which is shared for both configuration systems and holds allConfig
objects, -
its subclass
FaultToleranceOperation
, which only applies in case of the declarative API and holds allConfigDeclarativeOnly
objects.
In addition to the description above, implementations of the Config
interface (but not ConfigDeclarativeOnly
) also include a create()
method that takes a String
identifier and a Supplier
of the backing annotation instance.
This is supposed to be used to create an instance for configuring the programmatic API, because there’s no FaultToleranceMethod
in such case.
Also, a zero-configuration implementation of the Config
interface is generated, called …NoConfigImpl
.
This exists to be able to use the config interface even in the non-@ApplyGuard
case, where no configuration is possible.