Skip to content

Commit

Permalink
SmallRye Fault Tolerance: upgrade to 6.7.0
Browse files Browse the repository at this point in the history
This commit not only bumps the SmallRye Fault Tolerance version, but also
adapts the extension to the extensive changes made in this version.

This especially includes:

- deprecation of `FaultTolerance` and `@ApplyFaultTolerance`; replacements
  are `Guard`, `TypedGuard` and `@ApplyGuard`
- new `smallrye.faulttolerance.*` configuration properties, together with
  adaptation to `quarkus.fault-tolerance.*`

Most changes in this commit are added tests, which all adapt an already
existing test in SmallRye Fault Tolerance.
  • Loading branch information
Ladicek committed Nov 29, 2024
1 parent da9fbf0 commit 8549d79
Show file tree
Hide file tree
Showing 48 changed files with 2,126 additions and 35 deletions.
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
<smallrye-metrics.version>4.0.0</smallrye-metrics.version>
<smallrye-open-api.version>4.0.3</smallrye-open-api.version>
<smallrye-graphql.version>2.11.0</smallrye-graphql.version>
<smallrye-fault-tolerance.version>6.6.3</smallrye-fault-tolerance.version>
<smallrye-fault-tolerance.version>6.7.0</smallrye-fault-tolerance.version>
<smallrye-jwt.version>4.6.1</smallrye-jwt.version>
<smallrye-context-propagation.version>2.1.2</smallrye-context-propagation.version>
<smallrye-reactive-streams-operators.version>1.0.13</smallrye-reactive-streams-operators.version>
Expand Down
57 changes: 50 additions & 7 deletions docs/src/main/asciidoc/smallrye-fault-tolerance.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -535,11 +535,14 @@ public class CoffeeResource {
We can override the `maxRetries` parameter with 6 retries instead of 4 by the following configuration item:
[source,properties]
----
quarkus.fault-tolerance."org.acme.CoffeeResource/coffees".retry.max-retries=6
# alternatively, a specification-defined property can be used
org.acme.CoffeeResource/coffees/Retry/maxRetries=6
----

NOTE: The format is `fully-qualified-class-name/method-name/annotation-name/property-name=value`.
You can also configure a property for all the annotation via `annotation-name/property-name=value`.
The full set of Quarkus-native configuration properties is detailed in the <<configuration-reference>>.
The specification-defined configuration properties are still supported, but the Quarkus-native configuration takes priority.

== Conclusion

Expand Down Expand Up @@ -573,7 +576,7 @@ implementation("io.quarkus:quarkus-smallrye-fault-tolerance")
== Additional resources

SmallRye Fault Tolerance has more features than shown here.
Please check the link:https://smallrye.io/docs/smallrye-fault-tolerance/6.3.0/index.html[SmallRye Fault Tolerance documentation] to learn about them.
Please check the link:https://smallrye.io/docs/smallrye-fault-tolerance/6.7.0/index.html[SmallRye Fault Tolerance documentation] to learn about them.

In Quarkus, you can use the SmallRye Fault Tolerance optional features out of the box.

Expand All @@ -591,7 +594,7 @@ The entire point of context propagation is to make sure the new thread has the s
====

Non-compatible mode is enabled by default.
This means that methods that return `CompletionStage` (or `Uni`) have asynchronous fault tolerance applied without any `@Asynchronous`, `@AsynchronousNonBlocking`, `@Blocking` or `@NonBlocking` annotation.
This means that methods that return `CompletionStage` (or `Uni`) have asynchronous fault tolerance applied without the `@Asynchronous` or `@AsynchronousNonBlocking` annotation.
It also means that circuit breaker, fallback and retry automatically inspect the exception cause chain if the exception itself is insufficient to decide what should happen.

[NOTE]
Expand All @@ -601,16 +604,56 @@ To restore full compatibility, add this configuration property:
[source,properties]
----
smallrye.faulttolerance.mp-compatibility=true
quarkus.fault-tolerance.mp-compatibility=true
----
====

The link:https://smallrye.io/docs/smallrye-fault-tolerance/6.3.0/reference/programmatic-api.html[programmatic API] is present, including Mutiny support, and integrated with the declarative, annotation-based API.
You can use the `FaultTolerance` and `MutinyFaultTolerance` APIs out of the box.
The link:https://smallrye.io/docs/smallrye-fault-tolerance/6.7.0/reference/programmatic-api.html[programmatic API] is present and integrated with the declarative, annotation-based API.
You can use the `Guard`, `TypedGuard` and `@ApplyGuard` APIs out of the box.

Support for Kotlin is present (assuming you use the Quarkus extension for Kotlin), so you can guard your `suspend` functions with fault tolerance annotations.

Metrics are automatically discovered and integrated.
If your application uses the Quarkus extension for Micrometer, SmallRye Fault Tolerance will emit metrics to Micrometer.
If your application uses the Quarkus extension for SmallRye Metrics, SmallRye Fault Tolerance will emit metrics to MicroProfile Metrics.
Otherwise, SmallRye Fault Tolerance metrics will be disabled.

[[configuration-reference]]
== Configuration reference

The `<identifier>` below is:

* `"<classname>/<methodname>"` for per method configuration
* `"<classname>"` for per class configuration
* `global` for global configuration

include::{generated-dir}/config/quarkus-smallrye-fault-tolerance.adoc[opts=optional, leveloffset=+1]

This is how the specification-defined properties are mapped to the Quarkus-native configuration:

[%header,cols="1,1"]
|===
|Specification-defined config property
|Quarkus-native config property

|`<classname>/<methodname>/<annotation>/<member>`
|`quarkus.fault-tolerance."<classname>/<methodname>".<annotation>.<member>`

|`<classname>/<annotation>/<member>`
|`quarkus.fault-tolerance."<classname>".<annotation>.<member>`

|`<annotation>/<member>`
|`quarkus.fault-tolerance.global.<annotation>.<member>`

|`MP_Fault_Tolerance_NonFallback_Enabled`
|`quarkus.fault-tolerance.enabled`

|`MP_Fault_Tolerance_Metrics_Enabled`
|`quarkus.fault-tolerance.metrics.enabled`
|===

All the `<annotation>` and `<member>` parts are changed from camel case (`BeforeRetry`, `methodName`) to kebab case (`before-retry`, `method-name`).
Two annotation members are special cased to improve consistency:

- `Retry/durationUnit` moves to `retry.max-duration-unit`
- `Retry/jitterDelayUnit` moves to `retry.jitter-unit`
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.smallrye.faulttolerance.deployment;

import java.lang.annotation.Annotation;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.MethodInfo;

import io.smallrye.faulttolerance.config.ConfigPrefix;
import io.smallrye.faulttolerance.config.NewConfig;

// copy of `io.smallrye.faulttolerance.config.ConfigUtil` and translation from reflection to Jandex
final class ConfigUtilJandex {
static final String GLOBAL = "global";

static String newKey(Class<? extends Annotation> annotation, String member, MethodInfo declaringMethod) {
return ConfigPrefix.VALUE + "\"" + declaringMethod.declaringClass().name() + "/" + declaringMethod.name()
+ "\"." + NewConfig.get(annotation, member);
}

static String oldKey(Class<? extends Annotation> annotation, String member, MethodInfo declaringMethod) {
return declaringMethod.declaringClass().name() + "/" + declaringMethod.name()
+ "/" + annotation.getSimpleName() + "/" + member;
}

static String newKey(Class<? extends Annotation> annotation, String member, ClassInfo declaringClass) {
return ConfigPrefix.VALUE + "\"" + declaringClass.name() + "\"."
+ NewConfig.get(annotation, member);
}

static String oldKey(Class<? extends Annotation> annotation, String member, ClassInfo declaringClass) {
return declaringClass.name() + "/" + annotation.getSimpleName() + "/" + member;
}

static String newKey(Class<? extends Annotation> annotation, String member) {
return ConfigPrefix.VALUE + GLOBAL + "." + NewConfig.get(annotation, member);
}

static String oldKey(Class<? extends Annotation> annotation, String member) {
return annotation.getSimpleName() + "/" + member;
}

// no need to have the `isEnabled()` method, that is only needed at runtime
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import org.jboss.jandex.DotName;

import io.smallrye.common.annotation.Blocking;
import io.smallrye.common.annotation.Identifier;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.faulttolerance.FaultToleranceInterceptor;
import io.smallrye.faulttolerance.api.ApplyFaultTolerance;
import io.smallrye.faulttolerance.api.ApplyGuard;
import io.smallrye.faulttolerance.api.AsynchronousNonBlocking;
import io.smallrye.faulttolerance.api.BeforeRetry;
import io.smallrye.faulttolerance.api.BeforeRetryHandler;
Expand All @@ -23,21 +25,28 @@
import io.smallrye.faulttolerance.api.CustomBackoffStrategy;
import io.smallrye.faulttolerance.api.ExponentialBackoff;
import io.smallrye.faulttolerance.api.FibonacciBackoff;
import io.smallrye.faulttolerance.api.Guard;
import io.smallrye.faulttolerance.api.RateLimit;
import io.smallrye.faulttolerance.api.RetryWhen;
import io.smallrye.faulttolerance.api.TypedGuard;

public final class DotNames {
public static final DotName OBJECT = DotName.createSimple(Object.class);
public static final DotName IDENTIFIER = DotName.createSimple(Identifier.class);

public static final DotName FALLBACK_HANDLER = DotName.createSimple(FallbackHandler.class);
public static final DotName BEFORE_RETRY_HANDLER = DotName.createSimple(BeforeRetryHandler.class);

public static final DotName FAULT_TOLERANCE_INTERCEPTOR = DotName.createSimple(FaultToleranceInterceptor.class);

public static final DotName GUARD = DotName.createSimple(Guard.class);
public static final DotName TYPED_GUARD = DotName.createSimple(TypedGuard.class);

// ---
// fault tolerance annotations

public static final DotName APPLY_FAULT_TOLERANCE = DotName.createSimple(ApplyFaultTolerance.class);
public static final DotName APPLY_GUARD = DotName.createSimple(ApplyGuard.class);

public static final DotName ASYNCHRONOUS = DotName.createSimple(Asynchronous.class);
public static final DotName ASYNCHRONOUS_NON_BLOCKING = DotName.createSimple(AsynchronousNonBlocking.class);
Expand All @@ -62,7 +71,7 @@ public final class DotNames {
// certain SmallRye annotations (@CircuitBreakerName, @[Non]Blocking, @*Backoff, @RetryWhen, @BeforeRetry)
// do _not_ trigger the fault tolerance interceptor alone, only in combination
// with other fault tolerance annotations
public static final Set<DotName> FT_ANNOTATIONS = Set.of(APPLY_FAULT_TOLERANCE, ASYNCHRONOUS,
public static final Set<DotName> FT_ANNOTATIONS = Set.of(APPLY_FAULT_TOLERANCE, APPLY_GUARD, ASYNCHRONOUS,
ASYNCHRONOUS_NON_BLOCKING, BULKHEAD, CIRCUIT_BREAKER, FALLBACK, RATE_LIMIT, RETRY, TIMEOUT);

public static final Set<DotName> BACKOFF_ANNOTATIONS = Set.of(EXPONENTIAL_BACKOFF, FIBONACCI_BACKOFF, CUSTOM_BACKOFF);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.smallrye.common.annotation.Blocking;
import io.smallrye.common.annotation.NonBlocking;
import io.smallrye.faulttolerance.api.ApplyFaultTolerance;
import io.smallrye.faulttolerance.api.ApplyGuard;
import io.smallrye.faulttolerance.api.AsynchronousNonBlocking;
import io.smallrye.faulttolerance.api.BeforeRetry;
import io.smallrye.faulttolerance.api.CircuitBreakerName;
Expand Down Expand Up @@ -130,6 +131,7 @@ FaultToleranceMethod createFaultToleranceMethod(ClassInfo beanClass, MethodInfo
result.method = createMethodDescriptor(method);

result.applyFaultTolerance = getAnnotation(ApplyFaultTolerance.class, method, beanClass, annotationsPresentDirectly);
result.applyGuard = getAnnotation(ApplyGuard.class, method, beanClass, annotationsPresentDirectly);

result.asynchronous = getAnnotation(Asynchronous.class, method, beanClass, annotationsPresentDirectly);
result.asynchronousNonBlocking = getAnnotation(AsynchronousNonBlocking.class, method, beanClass,
Expand Down Expand Up @@ -236,18 +238,30 @@ private String getMethodNameFromConfig(MethodInfo method, Set<Class<? extends An
String result;
org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig();
if (annotationsPresentDirectly.contains(ftAnnotation)) {
// <classname>/<methodname>/<annotation>/<parameter>
String key = method.declaringClass().name() + "/" + method.name() + "/" + ftAnnotation.getSimpleName() + "/"
+ memberName;
result = config.getOptionalValue(key, String.class).orElse(null);
// smallrye.faulttolerance."<classname>/<methodname>".<annotation>.<member>
String newKey = ConfigUtilJandex.newKey(ftAnnotation, memberName, method);
// <classname>/<methodname>/<annotation>/<member>
String oldKey = ConfigUtilJandex.oldKey(ftAnnotation, memberName, method);
result = config.getOptionalValue(newKey, String.class)
.or(() -> config.getOptionalValue(oldKey, String.class))
.orElse(null);
} else {
// <classname>/<annotation>/<parameter>
String key = method.declaringClass().name() + "/" + ftAnnotation.getSimpleName() + "/" + memberName;
result = config.getOptionalValue(key, String.class).orElse(null);
// smallrye.faulttolerance."<classname>".<annotation>.<member>
String newKey = ConfigUtilJandex.newKey(ftAnnotation, memberName, method.declaringClass());
// <classname>/<annotation>/<member>
String oldKey = ConfigUtilJandex.oldKey(ftAnnotation, memberName, method.declaringClass());
result = config.getOptionalValue(newKey, String.class)
.or(() -> config.getOptionalValue(oldKey, String.class))
.orElse(null);
}
if (result == null) {
// <annotation>/<parameter>
result = config.getOptionalValue(ftAnnotation.getSimpleName() + "/" + memberName, String.class).orElse(null);
// smallrye.faulttolerance.global.<annotation>.<member>
String newKey = ConfigUtilJandex.newKey(ftAnnotation, memberName);
// <annotation>/<member>
String oldKey = ConfigUtilJandex.oldKey(ftAnnotation, memberName);
result = config.getOptionalValue(newKey, String.class)
.or(() -> config.getOptionalValue(oldKey, String.class))
.orElse(null);
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@
import io.quarkus.smallrye.faulttolerance.runtime.QuarkusFallbackHandlerProvider;
import io.quarkus.smallrye.faulttolerance.runtime.QuarkusFaultToleranceOperationProvider;
import io.quarkus.smallrye.faulttolerance.runtime.SmallRyeFaultToleranceRecorder;
import io.smallrye.faulttolerance.CdiFaultToleranceSpi;
import io.quarkus.smallrye.faulttolerance.runtime.config.SmallRyeFaultToleranceConfigRelocate;
import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.faulttolerance.CdiSpi;
import io.smallrye.faulttolerance.CircuitBreakerMaintenanceImpl;
import io.smallrye.faulttolerance.Enablement;
import io.smallrye.faulttolerance.ExecutorHolder;
import io.smallrye.faulttolerance.FaultToleranceBinding;
import io.smallrye.faulttolerance.FaultToleranceInterceptor;
Expand Down Expand Up @@ -94,6 +97,8 @@ public void build(BuildProducer<AnnotationsTransformerBuildItem> annotationsTran
ContextPropagationRequestContextControllerProvider.class.getName()));
serviceProvider.produce(new ServiceProviderBuildItem(RunnableWrapper.class.getName(),
ContextPropagationRunnableWrapper.class.getName()));
serviceProvider.produce(new ServiceProviderBuildItem(ConfigSourceInterceptor.class.getName(),
SmallRyeFaultToleranceConfigRelocate.class.getName()));
// make sure this is initialised at runtime, otherwise it will get a non-initialised ContextPropagationManager
runtimeInitializedClassBuildItems.produce(new RuntimeInitializedClassBuildItem(RunnableWrapper.class.getName()));

Expand Down Expand Up @@ -169,7 +174,8 @@ public void transform(TransformationContext context) {
QuarkusAsyncExecutorProvider.class,
CircuitBreakerMaintenanceImpl.class,
RequestContextIntegration.class,
SpecCompatibility.class);
SpecCompatibility.class,
Enablement.class);

if (metricsCapability.isEmpty()) {
builder.addBeanClass("io.smallrye.faulttolerance.metrics.NoopProvider");
Expand All @@ -187,8 +193,8 @@ public void transform(TransformationContext context) {
// are currently resolved dynamically at runtime because per the spec interceptor bindings cannot be declared on interfaces
beans.produce(AdditionalBeanBuildItem.builder().setUnremovable()
.addBeanClasses(FaultToleranceInterceptor.class, QuarkusFaultToleranceOperationProvider.class,
QuarkusExistingCircuitBreakerNames.class, CdiFaultToleranceSpi.EagerDependencies.class,
CdiFaultToleranceSpi.LazyDependencies.class)
QuarkusExistingCircuitBreakerNames.class, CdiSpi.EagerDependencies.class,
CdiSpi.LazyDependencies.class)
.build());

config.produce(new RunTimeConfigurationDefaultBuildItem("smallrye.faulttolerance.mp-compatibility", "false"));
Expand Down Expand Up @@ -262,7 +268,21 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
List<Throwable> exceptions = new ArrayList<>();
Map<String, Set<String>> existingCircuitBreakerNames = new HashMap<>();

Map<String, Set<String>> existingGuards = new HashMap<>();
Set<String> expectedGuards = new HashSet<>();

for (BeanInfo info : validationPhase.getContext().beans()) {
if (info.hasType(DotNames.GUARD) || info.hasType(DotNames.TYPED_GUARD)) {
info.getQualifier(DotNames.IDENTIFIER).ifPresent(idAnn -> {
String id = idAnn.value().asString();
existingGuards.computeIfAbsent(id, ignored -> new HashSet<>()).add(info.toString());
if ("global".equals(id)) {
exceptions.add(new DefinitionException("Guard/TypedGuard with identifier 'global' is not allowed: "
+ info));
}
});
}

ClassInfo beanClass = info.getImplClazz();
if (beanClass == null) {
continue;
Expand Down Expand Up @@ -309,6 +329,10 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
existingCircuitBreakerNames.computeIfAbsent(ann.value().asString(), ignored -> new HashSet<>())
.add(method + " @ " + method.declaringClass());
}

if (annotationStore.hasAnnotation(method, DotNames.APPLY_GUARD)) {
expectedGuards.add(annotationStore.getAnnotation(method, DotNames.APPLY_GUARD).value().asString());
}
}
});

Expand All @@ -322,6 +346,10 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
&& annotationStore.hasAnnotation(beanClass, DotNames.NON_BLOCKING)) {
exceptions.add(new DefinitionException("Both @Blocking and @NonBlocking present on '" + beanClass + "'"));
}

if (annotationStore.hasAnnotation(beanClass, DotNames.APPLY_GUARD)) {
expectedGuards.add(annotationStore.getAnnotation(beanClass, DotNames.APPLY_GUARD).value().asString());
}
}
}

Expand Down Expand Up @@ -353,6 +381,19 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
}
}

for (Map.Entry<String, Set<String>> entry : existingGuards.entrySet()) {
if (entry.getValue().size() > 1) {
exceptions.add(new DefinitionException("Multiple Guard/TypedGuard beans have the same identifier '"
+ entry.getKey() + "': " + entry.getValue()));
}
}
for (String expectedGuard : expectedGuards) {
if (!existingGuards.containsKey(expectedGuard)) {
exceptions.add(new DefinitionException("Guard/TypedGuard with identifier '" + expectedGuard
+ "' expected, but does not exist"));
}
}

if (!exceptions.isEmpty()) {
errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(exceptions));
}
Expand Down
Loading

0 comments on commit 8549d79

Please sign in to comment.