Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements #1084

Merged
merged 7 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions doc/modules/ROOT/pages/internals/core.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ NOTE: The `Future` type here is _not_ a `java.util.concurrent.Future`.
It comes from {smallrye-fault-tolerance} and it could be described as a very bare-bones variant of `CompletableFuture`, except it's split into `Completer` and `Future`.
It is not supposed to be used outside of this project.

The `FaultToleranceContext` is a `Supplier<Future<V>>` that represents the method invocation guarded by this fault tolerance strategy.
The fault tolerance strategy does its work around `ctx.get()`.
It can catch exceptions, invoke `ctx.get()` multiple times, invoke something else, etc.
The `FaultToleranceContext` is similar to a `Callable<Future<V>>`; it represents the method invocation guarded by this fault tolerance strategy.
The fault tolerance strategy does its work around `ctx.call()`.
It can catch exceptions, invoke `ctx.call()` multiple times, invoke something else, etc.
As an example, let's consider this strategy, applicable to methods that return a `String`:

[source,java]
Expand All @@ -28,7 +28,7 @@ public class MyStringFallback implements FaultToleranceStrategy<String> {
public Future<String> apply(FaultToleranceContext<String> ctx) {
Completer<String> completer = Completer.create();
try {
ctx.get().then((value, error) -> {
ctx.call().then((value, error) -> {
if (error == null) {
completer.complete(value);
} else {
Expand Down Expand Up @@ -81,9 +81,9 @@ public class MyStringFallback implements FaultToleranceStrategy<String> {

We see that one strategy delegates to another, passing the `FaultToleranceContext` along.
In fact, all the implementations in {smallrye-fault-tolerance} are written like this: they expect to be used in a chain, so they take another `FaultToleranceStrategy` to which they delegate.
But if all strategies have this form, when is `ctx.get()` actually invoked?
But if all strategies have this form, when is `ctx.call()` actually invoked?
Good question!
The ultimate `ctx.get()` invocation is done by a special fault tolerance strategy which is called, well, `Invocation`.
The ultimate `ctx.call()` invocation is done by a special fault tolerance strategy which is called, well, `Invocation`.

As an example which uses real {microprofile-fault-tolerance} annotations, let's consider this method:

Expand All @@ -105,7 +105,7 @@ Fallback(
Retry(
Timeout(
Invocation(
// ctx.get() will happen here
// ctx.call() will happen here
// that will, in turn, invoke doSomething()
)
)
Expand Down
40 changes: 39 additions & 1 deletion doc/modules/ROOT/pages/reference/reusable.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,45 @@ If the `Guard` or `TypedGuard` object used for `@ApplyGuard` is also used xref:r

== Kotlin `suspend` Functions

The `@ApplyGuard` API does not support Kotlin `suspend` functions at the moment.
Even though the programmatic API of `Guard` and `TypedGuard` does not support Kotlin `suspend` functions, the declarative API of `@ApplyGuard` does.
When the guard is a `Guard`, no restrictions apply.

When the guard is a `TypedGuard`, however, its type must be a synchronous return type of the `suspend` function.
For example, when the `suspend` function is declared to return a `String` asynchronously:

[source,kotlin]
----
@ApplyGuard("my-fault-tolerance")
@Fallback(fallbackMethod = "fallback")
suspend fun hello(): String {
delay(100)
throw IllegalArgumentException()
}
----

The `TypedGuard` must be declared to guard actions of type `String`:

[source,kotlin]
----
@Produces
@Identifier("my-fault-tolerance")
val GUARD = TypedGuard.create(String::class.java)
.withRetry().maxRetries(2).done()
.withFallback().handler(Supplier { "fallback" }).done()
.build()
----

This means that a possible fallback declared on the `TypedGuard` must be synchronous; it cannot be a `suspend` lambda.

The `@Fallback` method, if declared, must have a matching signature and so must be a `suspend` function:

[source,kotlin]
----
suspend fun fallback(): String {
delay(100)
return "fallback"
}
----

[[migration_from_applyfaulttolerance]]
== Migration from `@ApplyFaultTolerance`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import java.util.function.Consumer;
import java.util.function.Supplier;

public final class FaultToleranceContext<V> implements Supplier<Future<V>> {
public final class FaultToleranceContext<V> {
private final Supplier<Future<V>> delegate;
private final boolean isAsync;

Expand All @@ -16,8 +16,7 @@ public FaultToleranceContext(Supplier<Future<V>> delegate, boolean isAsync) {
this.isAsync = isAsync;
}

@Override
public Future<V> get() {
public Future<V> call() {
return delegate.get();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private Invocation() {
public Future<V> apply(FaultToleranceContext<V> ctx) {
LOG.trace("Guarded method invocation started");
try {
return ctx.get();
return ctx.call();
} catch (Exception e) {
return Future.ofError(e);
} finally {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.smallrye.faulttolerance.core.apiimpl;

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

import io.smallrye.faulttolerance.core.invocation.AsyncSupport;
import io.smallrye.faulttolerance.core.invocation.Invoker;

public final class AsyncInvocation<V, AT> {
public final AsyncSupport<V, AT> asyncSupport;
public final Invoker<AT> toFutureInvoker;
public final Object[] arguments; // to create a `StrategyInvoker`, which is always used as a `fromFutureInvoker`

public AsyncInvocation(AsyncSupport<V, AT> asyncSupport, Invoker<AT> toFutureInvoker, Object[] arguments) {
this.asyncSupport = checkNotNull(asyncSupport, "asyncSupport must be set");
this.toFutureInvoker = checkNotNull(toFutureInvoker, "toFutureInvoker must be set");
this.arguments = arguments;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,18 @@ 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,
static <V, T> AsyncInvocation<V, T> asyncInvocation(Callable<T> action, AsyncSupport<V, T> asyncSupport) {
return asyncSupport != null ? new AsyncInvocation<>(asyncSupport, new CallableInvoker<>(action), null) : null;
}

// 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> T guard(Callable<T> action, FaultToleranceStrategy<V> strategy, AsyncInvocation<V, T> asyncInvocation,
EventHandlers eventHandlers, Consumer<FaultToleranceContext<?>> contextModifier) throws Exception {
if (asyncSupport == null) {
if (asyncInvocation == null) {
FaultToleranceContext<T> ctx = new FaultToleranceContext<>(() -> Future.from(action), false);
if (contextModifier != null) {
contextModifier.accept(ctx);
Expand All @@ -58,14 +67,14 @@ 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);
AsyncSupport<V, T> asyncSupport = asyncInvocation.asyncSupport;
Invoker<T> toFutureInvoker = asyncInvocation.toFutureInvoker;
FaultToleranceContext<V> ctx = new FaultToleranceContext<>(() -> asyncSupport.toFuture(toFutureInvoker), true);
if (contextModifier != null) {
contextModifier.accept(ctx);
}
eventHandlers.register(ctx);
Invoker<Future<V>> wrapper = new StrategyInvoker<>(null, strategy, ctx);
return asyncSupport.fromFuture(wrapper);
Invoker<Future<V>> fromFutureInvoker = new StrategyInvoker<>(asyncInvocation.arguments, strategy, ctx);
return asyncSupport.fromFuture(fromFutureInvoker);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,27 +83,20 @@ public class GuardImpl implements Guard {
this.eventHandlers = eventHandlers;
}

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,
contextModifier);
}

@Override
public <T> T call(Callable<T> action, Class<T> type) throws Exception {
return guard(action, type, null);
return guard(action, type);
}

@Override
public <T> T call(Callable<T> action, TypeLiteral<T> type) throws Exception {
return guard(action, type.getType(), null);
return guard(action, type.getType());
}

@Override
public <T> T get(Supplier<T> action, Class<T> type) {
try {
return guard(action::get, type, null);
return guard(action::get, type);
} catch (Exception e) {
throw sneakyThrow(e);
}
Expand All @@ -112,12 +105,27 @@ public <T> T get(Supplier<T> action, Class<T> type) {
@Override
public <T> T get(Supplier<T> action, TypeLiteral<T> type) {
try {
return guard(action::get, type.getType(), null);
return guard(action::get, type.getType());
} catch (Exception e) {
throw sneakyThrow(e);
}
}

private <V, T> T guard(Callable<T> action, Type valueType) throws Exception {
FaultToleranceStrategy<V> castStrategy = (FaultToleranceStrategy<V>) strategy;

AsyncSupport<V, T> asyncSupport = GuardCommon.asyncSupport(valueType);
AsyncInvocation<V, T> asyncInvocation = GuardCommon.asyncInvocation(action, asyncSupport);
return GuardCommon.guard(action, castStrategy, asyncInvocation, eventHandlers, null);
}

public <V, T> T guard(Callable<T> action, AsyncInvocation<V, T> asyncInvocation,
Consumer<FaultToleranceContext<?>> contextModifier) throws Exception {
FaultToleranceStrategy<V> castStrategy = (FaultToleranceStrategy<V>) strategy;

return GuardCommon.guard(action, castStrategy, asyncInvocation, eventHandlers, contextModifier);
}

public static class BuilderImpl implements Builder {
private final BuilderEagerDependencies eagerDependencies;
private final Supplier<BuilderLazyDependencies> lazyDependencies;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,30 @@ public final class TypedGuardImpl<V, T> implements TypedGuard<T> {
this.eventHandlers = eventHandlers;
}

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

@Override
public T call(Callable<T> action) throws Exception {
return guard(action, null);
return guard(action);
}

@Override
public T get(Supplier<T> action) {
try {
return guard(action::get, null);
return guard(action::get);
} catch (Exception e) {
throw sneakyThrow(e);
}
}

private T guard(Callable<T> action) throws Exception {
AsyncInvocation<V, T> asyncInvocation = GuardCommon.asyncInvocation(action, asyncSupport);
return GuardCommon.guard(action, strategy, asyncInvocation, eventHandlers, null);
}

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

public static class BuilderImpl<V, T> implements TypedGuard.Builder<T> {
private final BuilderEagerDependencies eagerDependencies;
private final Supplier<BuilderLazyDependencies> lazyDependencies;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@ interface CdiLogger extends BasicLogger {

@Message(id = 8, value = "Multiple Guard/TypedGuard beans have the same identifier '%s': %s")
DefinitionException multipleGuardsWithTheSameIdentifier(String identifier, Set<String> beans);

@Message(id = 9, value = "Guard/TypedGuard with identifier '%s' expected, but does not exist")
DefinitionException expectedGuardDoesNotExist(String identifier);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,22 @@
import org.eclipse.microprofile.faulttolerance.ExecutionContext;

final class ExecutionContextImpl implements ExecutionContext {
private final InvocationContext interceptionContext;
private final InvocationContext invocationContext;
private final Throwable failure;

ExecutionContextImpl(InvocationContext interceptionContext, Throwable failure) {
this.interceptionContext = interceptionContext;
ExecutionContextImpl(InvocationContext invocationContext, Throwable failure) {
this.invocationContext = invocationContext;
this.failure = failure;
}

@Override
public Method getMethod() {
return interceptionContext.getMethod();
return invocationContext.getMethod();
}

@Override
public Object[] getParameters() {
return interceptionContext.getParameters();
return invocationContext.getParameters();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public class FaultToleranceExtension implements Extension {

private final ConcurrentMap<String, Set<String>> existingGuards = new ConcurrentHashMap<>();

private final Set<String> expectedGuards = ConcurrentHashMap.newKeySet();

private final Set<MetricsIntegration> metricsIntegrations;

private static boolean isPresent(String className) {
Expand Down Expand Up @@ -248,6 +250,10 @@ void collectFaultToleranceOperations(@Observes ProcessManagedBean<?> event) {
.add(annotatedMethod.getJavaMember().toGenericString());
}

if (operation.hasApplyGuard()) {
expectedGuards.add(operation.getApplyGuard().value());
}

for (Class<? extends Annotation> backoffAnnotation : BACKOFF_ANNOTATIONS) {
if (annotatedMethod.isAnnotationPresent(backoffAnnotation)
&& !annotatedMethod.isAnnotationPresent(Retry.class)) {
Expand Down Expand Up @@ -337,7 +343,13 @@ void validate(@Observes AfterDeploymentValidation event) {
entry.getKey(), entry.getValue()));
}
}
for (String expectedGuard : expectedGuards) {
if (!existingGuards.containsKey(expectedGuard)) {
event.addDeploymentProblem(LOG.expectedGuardDoesNotExist(expectedGuard));
}
}
existingGuards.clear();
expectedGuards.clear();
}

private static String getCacheKey(Class<?> beanClass, Method method) {
Expand Down
Loading