Skip to content

Commit

Permalink
enable declarative @fallback and thread offload with @ApplyGuard
Browse files Browse the repository at this point in the history
Declarative `@Fallback` is used with both `Guard` and `TypedGuard`
and if the `TypedGuard` defines a fallback, the `@Fallback` annotation
takes priority.

Declarative thread offload (figuring out when thread offload is needed
and when not) overrides the `Guard` / `TypedGuard` configuration
if the annotated method is determined to be asynchronous; only if
it is not the programmatic configuration is used.
  • Loading branch information
Ladicek committed Nov 21, 2024
1 parent 72293db commit 95e16fc
Show file tree
Hide file tree
Showing 85 changed files with 2,189 additions and 63 deletions.
26 changes: 21 additions & 5 deletions api/src/main/java/io/smallrye/faulttolerance/api/ApplyGuard.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import io.smallrye.common.annotation.Experimental;

/**
* A special interceptor binding annotation to apply preconfigured fault tolerance.
* An interceptor binding annotation to apply preconfigured fault tolerance.
* If {@code @ApplyGuard("<identifier>")} is present on a business method,
* then a bean of type {@link Guard} or {@link TypedGuard} with qualifier
* {@link io.smallrye.common.annotation.Identifier @Identifier("&lt;identifier>")}
Expand All @@ -30,12 +30,28 @@
* the one on the method takes precedence.
* <p>
* When {@code @ApplyGuard} applies to a business method, all other fault tolerance
* annotations that would otherwise also apply to that method are ignored.
* annotations that would also apply to that method are ignored, except:
*
* <ul>
* <li>{@link org.eclipse.microprofile.faulttolerance.Fallback @Fallback}</li>
* <li>{@link org.eclipse.microprofile.faulttolerance.Asynchronous @Asynchronous}</li>
* <li>{@link AsynchronousNonBlocking @AsynchronousNonBlocking}</li>
* </ul>
*
* If {@code @Fallback} is present, it is used both by {@code Guard} and {@code TypedGuard}
* and it overrides the possible fallback configuration of {@code TypedGuard}. Further,
* the thread offload configuration of {@code Guard} or {@code TypedGuard} is ignored
* if the annotated method is asynchronous as determined from the method signature
* and possible annotations ({@code @Asynchronous} and {@code @AsynchronousNonBlocking}).
* The thread offload configuration of {@code Guard} or {@code TypedGuard} is only honored
* when the method cannot be determined to be asynchronous, but it still declares
* an asynchronous return type.
* <p>
* A single preconfigured fault tolerance can be applied to multiple methods.
* If the preconfigured fault tolerance is of type {@code TypedGuard}, then all methods
* must have the same return type. If the preconfigured fault tolerance is of type {@code Guard},
* no such requirement applies; note that in this case, there is no way to define a fallback.
* If the preconfigured fault tolerance is a {@code TypedGuard}, then all methods
* must have the same return type, which must be equal to the type the {@code TypedGuard}
* was created with. If the preconfigured fault tolerance is of type {@code Guard},
* no such requirement applies.
* <p>
* Note that this annotation has the same differences to standard MicroProfile Fault Tolerance
* as {@code Guard} / {@code TypedGuard}:
Expand Down
11 changes: 11 additions & 0 deletions doc/modules/ROOT/pages/reference/programmatic-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,17 @@ In this case, it won't be possible to distinguish the different `Guard` objects

If no description is provided, a random UUID is used.

== Differences to the Specification

`Guard` and `TypedGuard` have the following differences to standard MicroProfile Fault Tolerance:

* asynchronous actions of type `java.util.concurrent.Future` are not supported;
* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode.

== Kotlin `suspend` Functions

The `Guard` and `TypedGuard` APIs do not support Kotlin `suspend` functions at the moment.

== Integration Concerns

Integration concerns, which are particularly interesting for users of the standalone implementation, are xref:integration/programmatic-api.adoc[described] in the integration section.
Expand Down
47 changes: 44 additions & 3 deletions doc/modules/ROOT/pages/reference/reusable.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Each method has its own bulkhead, circuit breaker and/or rate limit, which is of
The xref:reference/programmatic-api.adoc[programmatic API] of {smallrye-fault-tolerance} allows using a single `Guard` or `TypedGuard` object to guard multiple disparate actions, which allows reuse and state sharing.
It is possible to use a programmatically constructed `Guard` or `TypedGuard` object declaratively, using the `@ApplyGuard` annotation.

To be able to do that, we need a bean of type `Guard` with the `@Identifier` qualifier:
To be able to do that, we need a bean of type `Guard` (or `TypedGuard`) with the `@Identifier` qualifier:

[source,java]
----
Expand Down Expand Up @@ -46,10 +46,40 @@ public class MyService {
}
----

== Defining Fallback

Note that it is not possible to define a fallback on `Guard`, because fallback is tied to the action type.
It is possible to define a fallback on `TypedGuard`, because it can only be used to guard methods with a single return type, equal to the type the `TypedGuard` was created with.

However, the `@ApplyGuard` annotation pays attention to the `@Fallback` annotation.
If `@Fallback` is defined, it is used both by `Guard` and `TypedGuard` instances, and it overrides the possible fallback defined on the `TypedGuard`.

== Defining Thread Offload

Both `Guard` and `TypedGuard` allow enabling or disabling thread offload.
This is ignored by `@ApplyGuard` if the annotated method is asynchronous as determined from the method signature and possible annotations (`@Asynchronous` and `@AsynchronousNonBlocking`).
See xref:reference/asynchronous.adoc[] for more information about how that determination works.

The thread offload configuration of `Guard` or `TypedGuard` is only honored when the method cannot be determined to be asynchronous, but it still declares an asynchronous return type.

.Compatible Mode
****
In the compatible mode, methods are determined to be asynchronous when they are annotated `@Asynchronous` or `@AsynchronousNonBlocking` and they declare an asynchronous return type.
For such methods, the `Guard` / `TypedGuard` thread offload configuration does not apply.
For methods that declare an asynchronous return type but are not annotated with `@Asynchronous` or `@AsynchronousNonBlocking`, the configuration on `Guard` / `TypedGuard` is honored.
****

It is also possible to create a bean of type `TypedGuard<>` and apply it to methods that return the type `TypedGuard` was created with.
Note that `TypedGuard` allows defining a fallback, because it can only be used to guard methods with a single return type.
.Non-compatible Mode
****
In the non-compatible mode, methods are determined to be asynchronous whenever they declare an asynchronous return type.
The `@Asynchronous` and `@AsynchronousNonBlocking` only affect whether the invocation of that method is offloaded to an extra thread.
If none of these annotations is present, no thread offload happens.
In other words, in non-compatible mode, the `Guard` / `TypedGuard` thread offload configuration never applies.
See xref:reference/non-compat.adoc[] for more information about the non-compatible mode.
****

== Metrics

Expand All @@ -61,6 +91,17 @@ All methods annotated `@ApplyGuard` share the same bulkhead, circuit breaker and

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.

== Differences to the Specification

`@ApplyGuard` has the same differences to standard MicroProfile Fault Tolerance as `Guard` / `TypedGuard`:

* asynchronous actions of type `java.util.concurrent.Future` are not supported;
* the fallback, circuit breaker and retry strategies always inspect the cause chain of exceptions, following the behavior of SmallRye Fault Tolerance in the non-compatible mode.

== Kotlin `suspend` Functions

The `@ApplyGuard` API does not support Kotlin `suspend` functions at the moment.

[[migration_from_applyfaulttolerance]]
== Migration from `@ApplyFaultTolerance`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import io.smallrye.faulttolerance.api.CustomBackoffStrategy;
import io.smallrye.faulttolerance.api.FaultTolerance;
import io.smallrye.faulttolerance.api.RateLimitType;
import io.smallrye.faulttolerance.core.FailureContext;
import io.smallrye.faulttolerance.core.FaultToleranceContext;
import io.smallrye.faulttolerance.core.FaultToleranceStrategy;
import io.smallrye.faulttolerance.core.Future;
Expand All @@ -31,6 +30,7 @@
import io.smallrye.faulttolerance.core.circuit.breaker.CircuitBreaker;
import io.smallrye.faulttolerance.core.circuit.breaker.CircuitBreakerEvents;
import io.smallrye.faulttolerance.core.fallback.Fallback;
import io.smallrye.faulttolerance.core.fallback.FallbackFunction;
import io.smallrye.faulttolerance.core.invocation.AsyncSupport;
import io.smallrye.faulttolerance.core.invocation.AsyncSupportRegistry;
import io.smallrye.faulttolerance.core.invocation.ConstantInvoker;
Expand Down Expand Up @@ -364,7 +364,7 @@ private FaultToleranceStrategy<T> buildSyncStrategy(BuilderLazyDependencies lazy

// fallback is always enabled
if (fallbackBuilder != null) {
Function<FailureContext, Future<T>> fallbackFunction = ctx -> {
FallbackFunction<T> fallbackFunction = ctx -> {
return Future.from(() -> fallbackBuilder.handler.apply(ctx.failure));
};
result = new Fallback<>(result, description, fallbackFunction,
Expand Down Expand Up @@ -449,7 +449,7 @@ private <V> FaultToleranceStrategy<V> buildAsyncStrategy(BuilderLazyDependencies
throw new FaultToleranceException("Unknown async type: " + asyncType);
}

Function<FailureContext, Future<V>> fallbackFunction = ctx -> {
FallbackFunction<V> fallbackFunction = ctx -> {
try {
return asyncSupport.toFuture(ConstantInvoker.of(
fallbackBuilder.handler.apply(ctx.failure)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.Callable;
import java.util.function.Consumer;

import io.smallrye.faulttolerance.core.FaultToleranceContext;
import io.smallrye.faulttolerance.core.FaultToleranceStrategy;
Expand All @@ -13,21 +14,22 @@
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.metrics.MeteredOperationName;

final class GuardCommon {
private static final Class<?>[] NO_PARAMS = new Class<?>[0];

// V = value type, e.g. String
// T = result type, e.g. String or CompletionStage<String> or Uni<String>
//
// in synchronous scenario, V = T
// in asynchronous scenario, T is an async type that eventually produces V
static <V, T> AsyncSupport<V, T> asyncSupport(Type type) {
if (type instanceof Class<?>) {
return AsyncSupportRegistry.get(new Class<?>[0], (Class<?>) type);
return AsyncSupportRegistry.get(NO_PARAMS, (Class<?>) type);
} else if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?> rawType = (Class<?>) parameterizedType.getRawType();
return AsyncSupportRegistry.get(new Class<?>[0], rawType);
return AsyncSupportRegistry.get(NO_PARAMS, rawType);
} else {
return null;
}
Expand All @@ -39,11 +41,11 @@ static <V, T> AsyncSupport<V, T> asyncSupport(Type type) {
// in synchronous scenario, V = T
// in asynchronous scenario, T is an async type that eventually produces V
static <V, T> T guard(Callable<T> action, FaultToleranceStrategy<V> strategy, AsyncSupport<V, T> asyncSupport,
EventHandlers eventHandlers, MeteredOperationName meteredOperationName) throws Exception {
EventHandlers eventHandlers, Consumer<FaultToleranceContext<?>> contextModifier) throws Exception {
if (asyncSupport == null) {
FaultToleranceContext<T> ctx = new FaultToleranceContext<>(() -> Future.from(action), false);
if (meteredOperationName != null) {
ctx.set(MeteredOperationName.class, meteredOperationName);
if (contextModifier != null) {
contextModifier.accept(ctx);
}
eventHandlers.register(ctx);
try {
Expand All @@ -59,8 +61,8 @@ static <V, T> T guard(Callable<T> action, FaultToleranceStrategy<V> strategy, As
Invoker<T> invoker = new CallableInvoker<>(action);
FaultToleranceContext<V> ctx = new FaultToleranceContext<>(() -> asyncSupport.toFuture(invoker), true);
ctx.set(AsyncSupport.class, asyncSupport);
if (meteredOperationName != null) {
ctx.set(MeteredOperationName.class, meteredOperationName);
if (contextModifier != null) {
contextModifier.accept(ctx);
}
eventHandlers.register(ctx);
Invoker<Future<V>> wrapper = new StrategyInvoker<>(null, strategy, ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,19 @@
import io.smallrye.faulttolerance.api.CustomBackoffStrategy;
import io.smallrye.faulttolerance.api.Guard;
import io.smallrye.faulttolerance.api.RateLimitType;
import io.smallrye.faulttolerance.core.FaultToleranceContext;
import io.smallrye.faulttolerance.core.FaultToleranceStrategy;
import io.smallrye.faulttolerance.core.async.RememberEventLoop;
import io.smallrye.faulttolerance.core.async.SyncAsyncSplit;
import io.smallrye.faulttolerance.core.async.ThreadOffload;
import io.smallrye.faulttolerance.core.bulkhead.Bulkhead;
import io.smallrye.faulttolerance.core.circuit.breaker.CircuitBreaker;
import io.smallrye.faulttolerance.core.circuit.breaker.CircuitBreakerEvents;
import io.smallrye.faulttolerance.core.fallback.Fallback;
import io.smallrye.faulttolerance.core.fallback.FallbackFunction;
import io.smallrye.faulttolerance.core.invocation.AsyncSupport;
import io.smallrye.faulttolerance.core.metrics.DelegatingMetricsCollector;
import io.smallrye.faulttolerance.core.metrics.MeteredOperation;
import io.smallrye.faulttolerance.core.metrics.MeteredOperationName;
import io.smallrye.faulttolerance.core.metrics.MetricsProvider;
import io.smallrye.faulttolerance.core.rate.limit.RateLimit;
import io.smallrye.faulttolerance.core.retry.BackOff;
Expand Down Expand Up @@ -81,10 +83,11 @@ public class GuardImpl implements Guard {
this.eventHandlers = eventHandlers;
}

public <V, T> T guard(Callable<T> action, Type valueType, MeteredOperationName meteredOperationName) throws Exception {
public <V, T> T guard(Callable<T> action, Type valueType, Consumer<FaultToleranceContext<?>> contextModifier)
throws Exception {
AsyncSupport<V, T> asyncSupport = GuardCommon.asyncSupport(valueType);
return GuardCommon.guard(action, (FaultToleranceStrategy<V>) strategy, asyncSupport, eventHandlers,
meteredOperationName);
contextModifier);
}

@Override
Expand Down Expand Up @@ -218,10 +221,8 @@ final <V> FaultToleranceStrategy<V> buildStrategy(BuilderLazyDependencies lazyDe
FaultToleranceStrategy<V> result = invocation();

// thread offload is always enabled
if (offloadToAnotherThread) {
Executor executor = offloadExecutor != null ? offloadExecutor : lazyDependencies.asyncExecutor();
result = new SyncAsyncSplit<>(new ThreadOffload<>(result, executor), result);
}
Executor executor = offloadExecutor != null ? offloadExecutor : lazyDependencies.asyncExecutor();
result = new SyncAsyncSplit<>(new ThreadOffload<>(result, executor, offloadToAnotherThread), result);

if (lazyDependencies.ftEnabled() && bulkheadBuilder != null) {
result = new Bulkhead<>(result, description,
Expand Down Expand Up @@ -275,7 +276,8 @@ final <V> FaultToleranceStrategy<V> buildStrategy(BuilderLazyDependencies lazyDe
retryBuilder.beforeRetry != null ? ctx -> retryBuilder.beforeRetry.accept(ctx.failure) : null);
}

// no fallback here
// fallback is always enabled
result = new Fallback<>(result, description, FallbackFunction.ignore(), ExceptionDecision.IGNORE);

MetricsProvider metricsProvider = lazyDependencies.metricsProvider();
if (metricsProvider.isEnabled()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import io.smallrye.faulttolerance.api.CustomBackoffStrategy;
import io.smallrye.faulttolerance.api.RateLimitType;
import io.smallrye.faulttolerance.api.TypedGuard;
import io.smallrye.faulttolerance.core.FailureContext;
import io.smallrye.faulttolerance.core.FaultToleranceContext;
import io.smallrye.faulttolerance.core.FaultToleranceStrategy;
import io.smallrye.faulttolerance.core.Future;
import io.smallrye.faulttolerance.core.async.RememberEventLoop;
Expand All @@ -30,11 +30,11 @@
import io.smallrye.faulttolerance.core.circuit.breaker.CircuitBreaker;
import io.smallrye.faulttolerance.core.circuit.breaker.CircuitBreakerEvents;
import io.smallrye.faulttolerance.core.fallback.Fallback;
import io.smallrye.faulttolerance.core.fallback.FallbackFunction;
import io.smallrye.faulttolerance.core.invocation.AsyncSupport;
import io.smallrye.faulttolerance.core.invocation.ConstantInvoker;
import io.smallrye.faulttolerance.core.metrics.DelegatingMetricsCollector;
import io.smallrye.faulttolerance.core.metrics.MeteredOperation;
import io.smallrye.faulttolerance.core.metrics.MeteredOperationName;
import io.smallrye.faulttolerance.core.metrics.MetricsProvider;
import io.smallrye.faulttolerance.core.rate.limit.RateLimit;
import io.smallrye.faulttolerance.core.retry.BackOff;
Expand Down Expand Up @@ -91,8 +91,8 @@ public final class TypedGuardImpl<V, T> implements TypedGuard<T> {
this.eventHandlers = eventHandlers;
}

public T guard(Callable<T> action, MeteredOperationName meteredOperationName) throws Exception {
return GuardCommon.guard(action, strategy, asyncSupport, eventHandlers, meteredOperationName);
public T guard(Callable<T> action, Consumer<FaultToleranceContext<?>> contextModifier) throws Exception {
return GuardCommon.guard(action, strategy, asyncSupport, eventHandlers, contextModifier);
}

@Override
Expand Down Expand Up @@ -223,10 +223,8 @@ final FaultToleranceStrategy<V> buildStrategy(BuilderLazyDependencies lazyDepend
FaultToleranceStrategy<V> result = invocation();

// thread offload is always enabled
if (offloadToAnotherThread) {
Executor executor = offloadExecutor != null ? offloadExecutor : lazyDependencies.asyncExecutor();
result = new SyncAsyncSplit<>(new ThreadOffload<>(result, executor), result);
}
Executor executor = offloadExecutor != null ? offloadExecutor : lazyDependencies.asyncExecutor();
result = new SyncAsyncSplit<>(new ThreadOffload<>(result, executor, offloadToAnotherThread), result);

if (lazyDependencies.ftEnabled() && bulkheadBuilder != null) {
result = new Bulkhead<>(result, description,
Expand Down Expand Up @@ -281,8 +279,9 @@ final FaultToleranceStrategy<V> buildStrategy(BuilderLazyDependencies lazyDepend
}

// fallback is always enabled
FallbackFunction<V> fallbackFunction = FallbackFunction.ignore();
ExceptionDecision exceptionDecision = ExceptionDecision.IGNORE;
if (fallbackBuilder != null) {
Function<FailureContext, Future<V>> fallbackFunction;
if (asyncSupport != null) {
fallbackFunction = ctx -> {
try {
Expand All @@ -298,10 +297,10 @@ final FaultToleranceStrategy<V> buildStrategy(BuilderLazyDependencies lazyDepend
};
}

result = new Fallback<>(result, description, fallbackFunction,
createExceptionDecision(fallbackBuilder.skipOn, fallbackBuilder.applyOn,
fallbackBuilder.whenPredicate));
exceptionDecision = createExceptionDecision(fallbackBuilder.skipOn, fallbackBuilder.applyOn,
fallbackBuilder.whenPredicate);
}
result = new Fallback<>(result, description, fallbackFunction, exceptionDecision);

MetricsProvider metricsProvider = lazyDependencies.metricsProvider();
if (metricsProvider.isEnabled()) {
Expand Down
Loading

0 comments on commit 95e16fc

Please sign in to comment.