Skip to content

Commit

Permalink
add support for configuration to @ApplyGuard
Browse files Browse the repository at this point in the history
  • Loading branch information
Ladicek committed Jan 15, 2025
1 parent 6e69f87 commit e373313
Show file tree
Hide file tree
Showing 119 changed files with 3,148 additions and 1,079 deletions.
38 changes: 28 additions & 10 deletions doc/modules/ROOT/pages/internals/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,24 @@ Note that CDI extensions may add, remove or modify annotations, so `FaultToleran
In environments that bootstrap the CDI container during application build, such as Quarkus, the set of all ``FaultToleranceMethod``s in the application should be collected during build and transferred to runtime.
Then, at application runtime, ``FaultToleranceOperation``s 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.
{smallrye-fault-tolerance} doesn't do anything like that, but is designed with Quarkus (and 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.
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:

[source,java]
----
@AutoConfig
interface FallbackConfig extends Fallback, Config {
interface CircuitBreakerConfig extends CircuitBreaker, Config {
default void validate() {
...
}
Expand All @@ -62,23 +62,23 @@ interface FallbackConfig extends Fallback, Config {
The config interface must:

. be annotated with `@AutoConfig`;
. extend the annotation interface (`Fallback` in this case);
. extend the `Config` interface (from the `autoconfig/core` module);
. extend the annotation interface (`CircuitBreaker` in this case);
. extend the `Config` (or `ConfigDeclarativeOnly`) interface (from the `autoconfig/core` module);
. 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`.
In this case, it would be called `CircuitBreakerConfigImpl`.

This implementation class has a `static` method called `create` that accepts a `FaultToleranceMethod`.
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`.
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`.
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.
Expand All @@ -97,3 +97,21 @@ For each fault tolerance strategy in the chain, the interceptor looks up configu

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 all `Config` objects,
* its subclass `FaultToleranceOperation`, which only applies in case of the declarative API and holds all `ConfigDeclarativeOnly` 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.
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/reference/config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Or globally:

[source,properties]
----
smallrye.faulttolerance.global.retry.enabled=5
smallrye.faulttolerance.global.retry.enabled=false
# alternatively, a specification-defined property can be used
Retry/enabled=false
Expand Down
91 changes: 90 additions & 1 deletion doc/modules/ROOT/pages/reference/reusable.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,95 @@ In other words, in non-compatible mode, the `Guard` / `TypedGuard` thread offloa
See xref:reference/non-compat.adoc[] for more information about the non-compatible mode.
****

== Configuration

Even though the programmatic API of `Guard` and `TypedGuard` does not support configuration, the declarative API of `@ApplyGuard` does.

The configuration closely resembles xref:reference/config.adoc[the configuration of the declarative API], where instead of the `<classname>` or `<classname>/<methodname>`, you use the identifier that's used in the `@Identifier` qualifier and in the `@ApplyGuard` annotation.

CAUTION: When configuring the `Guard` or `TypedGuard` using the following configuration properties, it is recommended to never use the `[Typed]Guard` programmatically.
The reason is that configuration is applied on the first invocation, and if that invocation is programmatic, the configuration key is unknown.

For example, let's assume the following preconfigured `Guard` object:

[source,java]
----
@ApplicationScoped
public class PreconfiguredFaultTolerance {
@Produces
@Identifier("my-fault-tolerance")
public static final Guard GUARD = Guard.create()
.withRetry().maxRetries(2).done()
.withTimeout().done()
.build();
}
----

This `Guard` supports retry and timeout; any attempt to configure other strategies will be ignored.

To reconfigure the number of maximum retries, the following configuration properties may be used:

[source,properties]
----
smallrye.faulttolerance."my-fault-tolerance".retry.max-retries=5
# alternatively, a specification-defined property can be used
my-fault-tolerance/Retry/maxRetries=5
----

Global configuration that applies to all usages of retry is also respected by `@ApplyGuard`:

[source,properties]
----
smallrye.faulttolerance.global.retry.max-retries=5
# alternatively, a specification-defined property can be used
Retry/maxRetries=5
----

Note that the configuration keys follow the configuration of the declarative API, even if the methods in the programmatic API builders are named differently.
For example, to set the rate limit in the programmatic API, the `RateLimitBuilder` uses a method called `limit()`, while the corresponding annotation member of `@RateLimit` is named `value()`.
The configuration key in both cases is `rate-limit.value` (or `RateLimit/value`).

=== Disabling Fault Tolerance

Configuration properties for disabling individual strategies also work:

[source,properties]
----
smallrye.faulttolerance."my-fault-tolerance".retry.enabled=false
# alternatively, a specification-defined property can be used
my-fault-tolerance/Retry/enabled=false
----

Or globally:

[source,properties]
----
smallrye.faulttolerance.global.retry.enabled=false
# alternatively, a specification-defined property can be used
Retry/enabled=false
----

=== Supported Strategies

The following fault tolerance strategies, when defined on a `Guard` (or `TypedGuard`) and used through `@ApplyGuard`, may be configured:

* bulkhead (except of `BulkheadBuilder.enableSynchronousQueueing()`)
* circuit breaker
* rate limit
* retry, including exponential backoff and Fibonacci backoff
* timeout

In the programmatic API, the following strategies are not built declaratively, and so their configuration is ignored:

* fallback
* circuit breaker name (cannot be configured in the declarative API either)
* certain parts of retry: custom backoff, before retry action and retry predicates
* thread offload

== Metrics

Methods annotated `@ApplyGuard` gather metrics similarly to methods annotated with {microprofile-fault-tolerance} annotations.
Expand All @@ -89,7 +178,7 @@ That is, each method gets its own metrics, with the `method` tag being `<fully q
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 xref:reference/programmatic-api.adoc[programmatically], that usage is coalesced in metrics under the description as the `method` tag.
If the `Guard` or `TypedGuard` object used for `@ApplyGuard` is also used xref:reference/programmatic-api.adoc[programmatically], that usage is coalesced in metrics (where the `method` tag is set to the description).

== Differences to the Specification

Expand Down
76 changes: 76 additions & 0 deletions implementation/apiimpl/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-fault-tolerance-implementation-parent</artifactId>
<version>6.7.4-SNAPSHOT</version>
</parent>

<artifactId>smallrye-fault-tolerance-apiimpl</artifactId>

<name>SmallRye Fault Tolerance: Programmatic API Implementation</name>

<dependencies>
<dependency>
<groupId>org.eclipse.microprofile.fault-tolerance</groupId>
<artifactId>microprofile-fault-tolerance-api</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-fault-tolerance-api</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-fault-tolerance-core</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-fault-tolerance-autoconfig-core</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-fault-tolerance-autoconfig-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</path>
<path>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging-processor</artifactId>
</path>

<path>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-fault-tolerance-autoconfig-processor</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import static io.smallrye.faulttolerance.core.util.Preconditions.checkNotNull;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import java.util.Collection;
import java.util.Set;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import io.smallrye.faulttolerance.core.metrics.MeteredOperation;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

// dependencies that may be accessed eagerly; these must be safe to use during static initialization
public interface BuilderEagerDependencies {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import java.util.concurrent.ExecutorService;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import java.util.concurrent.Callable;
import java.util.function.Function;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import java.util.function.Consumer;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import static io.smallrye.faulttolerance.core.Invocation.invocation;
import static io.smallrye.faulttolerance.core.util.Durations.timeInMillis;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package io.smallrye.faulttolerance.core.apiimpl;
package io.smallrye.faulttolerance.apiimpl;

import static io.smallrye.faulttolerance.core.util.SneakyThrow.sneakyThrow;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import java.util.function.Predicate;

import io.smallrye.faulttolerance.core.FaultToleranceContext;
import io.smallrye.faulttolerance.core.FaultToleranceStrategy;
Expand All @@ -14,6 +15,12 @@
import io.smallrye.faulttolerance.core.invocation.AsyncSupportRegistry;
import io.smallrye.faulttolerance.core.invocation.Invoker;
import io.smallrye.faulttolerance.core.invocation.StrategyInvoker;
import io.smallrye.faulttolerance.core.util.ExceptionDecision;
import io.smallrye.faulttolerance.core.util.PredicateBasedExceptionDecision;
import io.smallrye.faulttolerance.core.util.PredicateBasedResultDecision;
import io.smallrye.faulttolerance.core.util.ResultDecision;
import io.smallrye.faulttolerance.core.util.SetBasedExceptionDecision;
import io.smallrye.faulttolerance.core.util.SetOfThrowables;

final class GuardCommon {
private static final Class<?>[] NO_PARAMS = new Class<?>[0];
Expand Down Expand Up @@ -77,4 +84,35 @@ static <V, T> T guard(Callable<T> action, FaultToleranceStrategy<V> strategy, As
Invoker<Future<V>> fromFutureInvoker = new StrategyInvoker<>(asyncInvocation.arguments, strategy, ctx);
return asyncSupport.fromFuture(fromFutureInvoker);
}

// ---

static ResultDecision createResultDecision(Predicate<Object> whenResultPredicate) {
if (whenResultPredicate != null) {
// the builder API accepts a predicate that returns `true` when a result is considered failure,
// but `Retry` accepts a predicate that returns `true` when a result is considered success,
// hence the negation
return new PredicateBasedResultDecision(whenResultPredicate.negate());
}
return ResultDecision.ALWAYS_EXPECTED;
}

static ExceptionDecision createExceptionDecision(Class<? extends Throwable>[] consideredExpected,
Class<? extends Throwable>[] consideredFailure, Predicate<Throwable> whenExceptionPredicate) {
if (whenExceptionPredicate != null) {
// the builder API accepts a predicate that returns `true` when an exception is considered failure,
// but `PredicateBasedExceptionDecision` accepts a predicate that returns `true` when an exception
// is considered success -- hence the negation
return new PredicateBasedExceptionDecision(whenExceptionPredicate.negate());
}
return new SetBasedExceptionDecision(createSetOfThrowables(consideredFailure),
createSetOfThrowables(consideredExpected), true);
}

private static SetOfThrowables createSetOfThrowables(Class<? extends Throwable>[] throwableClasses) {
if (throwableClasses == null || throwableClasses.length == 0) {
return SetOfThrowables.EMPTY;
}
return SetOfThrowables.create(throwableClasses);
}
}
Loading

0 comments on commit e373313

Please sign in to comment.