Skip to content

Commit

Permalink
SmallRye Fault Tolerance: upgrade to 6.5.0 (and MP FT 4.1.1)
Browse files Browse the repository at this point in the history
This commit includes build-time resolution of fault tolerance methods
(`@Fallback.fallbackMethod()`, `@BeforeRetry.methodName()`). This means
that the corresponding configuration properties are now build-time only,
instead of runtime. This is a breaking change, but since we need to
register these methods for reflection for native image compilation,
there's unfortunately no other way.
  • Loading branch information
Ladicek committed Oct 14, 2024
1 parent 84e3545 commit 0a67472
Show file tree
Hide file tree
Showing 8 changed files with 568 additions and 47 deletions.
4 changes: 2 additions & 2 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<microprofile-metrics-api.version>4.0.1</microprofile-metrics-api.version>
<microprofile-context-propagation.version>1.3</microprofile-context-propagation.version>
<microprofile-opentracing-api.version>3.0</microprofile-opentracing-api.version>
<microprofile-fault-tolerance-api.version>4.0.2</microprofile-fault-tolerance-api.version>
<microprofile-fault-tolerance-api.version>4.1.1</microprofile-fault-tolerance-api.version>
<microprofile-reactive-streams-operators.version>3.0.1</microprofile-reactive-streams-operators.version>
<microprofile-rest-client.version>3.0.1</microprofile-rest-client.version>
<microprofile-jwt.version>2.1</microprofile-jwt.version>
Expand All @@ -58,7 +58,7 @@
<smallrye-metrics.version>4.0.0</smallrye-metrics.version>
<smallrye-open-api.version>3.13.0</smallrye-open-api.version>
<smallrye-graphql.version>2.10.0</smallrye-graphql.version>
<smallrye-fault-tolerance.version>6.4.1</smallrye-fault-tolerance.version>
<smallrye-fault-tolerance.version>6.5.0</smallrye-fault-tolerance.version>
<smallrye-jwt.version>4.6.0</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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.quarkus.smallrye.faulttolerance.deployment;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;

import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.faulttolerance.Asynchronous;
import org.eclipse.microprofile.faulttolerance.Bulkhead;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
Expand All @@ -19,7 +23,9 @@
import org.jboss.jandex.Type;

import io.quarkus.arc.processor.AnnotationStore;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.AnnotationProxyBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.gizmo.ClassOutput;
import io.smallrye.common.annotation.Blocking;
Expand All @@ -45,13 +51,19 @@ final class FaultToleranceScanner {

private final RecorderContext recorderContext;

private final BuildProducer<ReflectiveMethodBuildItem> reflectiveMethod;

private final FaultToleranceMethodSearch methodSearch;

FaultToleranceScanner(IndexView index, AnnotationStore annotationStore, AnnotationProxyBuildItem proxy,
ClassOutput output, RecorderContext recorderContext) {
ClassOutput output, RecorderContext recorderContext, BuildProducer<ReflectiveMethodBuildItem> reflectiveMethod) {
this.index = index;
this.annotationStore = annotationStore;
this.proxy = proxy;
this.output = output;
this.recorderContext = recorderContext;
this.reflectiveMethod = reflectiveMethod;
this.methodSearch = new FaultToleranceMethodSearch(index);
}

boolean hasFTAnnotations(ClassInfo clazz) {
Expand Down Expand Up @@ -141,6 +153,8 @@ FaultToleranceMethod createFaultToleranceMethod(ClassInfo beanClass, MethodInfo

result.annotationsPresentDirectly = annotationsPresentDirectly;

searchForMethods(result, beanClass, method, annotationsPresentDirectly);

return result;
}

Expand Down Expand Up @@ -169,6 +183,92 @@ private <A extends Annotation> A getAnnotation(Class<A> annotationType, MethodIn
return getAnnotationFromClass(annotationType, beanClass);
}

// ---

private void searchForMethods(FaultToleranceMethod result, ClassInfo beanClass, MethodInfo method,
Set<Class<? extends Annotation>> annotationsPresentDirectly) {
if (result.fallback != null) {
String fallbackMethod = getMethodNameFromConfig(method, annotationsPresentDirectly,
Fallback.class, "fallbackMethod");
if (fallbackMethod == null) {
fallbackMethod = result.fallback.fallbackMethod();
}
if (fallbackMethod != null && !fallbackMethod.isEmpty()) {
ClassInfo declaringClass = method.declaringClass();
Type[] parameterTypes = method.parameterTypes().toArray(new Type[0]);
Type returnType = method.returnType();
MethodInfo foundMethod = methodSearch.findFallbackMethod(beanClass,
declaringClass, fallbackMethod, parameterTypes, returnType);
Set<MethodInfo> foundMethods = methodSearch.findFallbackMethodsWithExceptionParameter(beanClass,
declaringClass, fallbackMethod, parameterTypes, returnType);
result.fallbackMethod = createMethodDescriptorIfNotNull(foundMethod);
result.fallbackMethodsWithExceptionParameter = createMethodDescriptorsIfNotEmpty(foundMethods);
if (foundMethod != null) {
reflectiveMethod.produce(new ReflectiveMethodBuildItem("@Fallback method", foundMethod));
}
for (MethodInfo m : foundMethods) {
reflectiveMethod.produce(new ReflectiveMethodBuildItem("@Fallback method", m));
}
}
}

if (result.beforeRetry != null) {
String beforeRetryMethod = getMethodNameFromConfig(method, annotationsPresentDirectly,
BeforeRetry.class, "methodName");
if (beforeRetryMethod == null) {
beforeRetryMethod = result.beforeRetry.methodName();
}
if (beforeRetryMethod != null && !beforeRetryMethod.isEmpty()) {
MethodInfo foundMethod = methodSearch.findBeforeRetryMethod(beanClass,
method.declaringClass(), beforeRetryMethod);
result.beforeRetryMethod = createMethodDescriptorIfNotNull(foundMethod);
if (foundMethod != null) {
reflectiveMethod.produce(new ReflectiveMethodBuildItem("@BeforeRetry method", foundMethod));
}
}
}
}

// copy of generated code to obtain a config value and translation from reflection to Jandex
// no need to check whether `ftAnnotation` is enabled, this will happen at runtime
private String getMethodNameFromConfig(MethodInfo method, Set<Class<? extends Annotation>> annotationsPresentDirectly,
Class<? extends Annotation> ftAnnotation, String memberName) {
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);
} else {
// <classname>/<annotation>/<parameter>
String key = method.declaringClass().name() + "/" + ftAnnotation.getSimpleName() + "/" + memberName;
result = config.getOptionalValue(key, String.class).orElse(null);
}
if (result == null) {
// <annotation>/<parameter>
result = config.getOptionalValue(ftAnnotation.getSimpleName() + "/" + memberName, String.class).orElse(null);
}
return result;
}

private MethodDescriptor createMethodDescriptorIfNotNull(MethodInfo method) {
return method == null ? null : createMethodDescriptor(method);
}

private List<MethodDescriptor> createMethodDescriptorsIfNotEmpty(Collection<MethodInfo> methods) {
if (methods.isEmpty()) {
return null;
}
List<MethodDescriptor> result = new ArrayList<>(methods.size());
for (MethodInfo method : methods) {
result.add(createMethodDescriptor(method));
}
return result;
}

// ---

private <A extends Annotation> A getAnnotationFromClass(Class<A> annotationType, ClassInfo clazz) {
DotName annotationName = DotName.createSimple(annotationType);
if (annotationStore.hasAnnotation(clazz, annotationName)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package io.quarkus.smallrye.faulttolerance.deployment;

import java.time.temporal.ChronoUnit;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Queue;
import java.util.Set;

import jakarta.annotation.Priority;
Expand All @@ -18,7 +16,6 @@
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
Expand Down Expand Up @@ -88,7 +85,6 @@ public void build(BuildProducer<AnnotationsTransformerBuildItem> annotationsTran
BuildProducer<SystemPropertyBuildItem> systemProperty,
CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ReflectiveMethodBuildItem> reflectiveMethod,
BuildProducer<RunTimeConfigurationDefaultBuildItem> config,
BuildProducer<RuntimeInitializedClassBuildItem> runtimeInitializedClassBuildItems) {

Expand All @@ -104,6 +100,8 @@ public void build(BuildProducer<AnnotationsTransformerBuildItem> annotationsTran
IndexView index = combinedIndexBuildItem.getIndex();

// Add reflective access to fallback handlers and before retry handlers
// (reflective access to fallback methods and before retry methods is added
// in `FaultToleranceScanner.searchForMethods`)
Set<String> handlers = new HashSet<>();
for (ClassInfo implementor : index.getAllKnownImplementors(DotNames.FALLBACK_HANDLER)) {
handlers.add(implementor.name().toString());
Expand All @@ -120,43 +118,6 @@ public void build(BuildProducer<AnnotationsTransformerBuildItem> annotationsTran
}
beans.produce(handlerBeans.build());
}
// Add reflective access to fallback methods
for (AnnotationInstance annotation : index.getAnnotations(DotNames.FALLBACK)) {
AnnotationValue fallbackMethodValue = annotation.value("fallbackMethod");
if (fallbackMethodValue == null) {
continue;
}
String fallbackMethod = fallbackMethodValue.asString();

Queue<DotName> classesToScan = new ArrayDeque<>(); // work queue

// @Fallback can only be present on methods, so this is just future-proofing
AnnotationTarget target = annotation.target();
if (target.kind() == Kind.METHOD) {
classesToScan.add(target.asMethod().declaringClass().name());
}

while (!classesToScan.isEmpty()) {
DotName name = classesToScan.poll();
ClassInfo clazz = index.getClassByName(name);
if (clazz == null) {
continue;
}

// we could further restrict the set of registered methods based on matching parameter types,
// but that's relatively complex and SmallRye Fault Tolerance has to do it anyway
clazz.methods()
.stream()
.filter(it -> fallbackMethod.equals(it.name()))
.forEach(it -> reflectiveMethod.produce(new ReflectiveMethodBuildItem(getClass().getName(), it)));

DotName superClass = clazz.superName();
if (superClass != null && !DotNames.OBJECT.equals(superClass)) {
classesToScan.add(superClass);
}
classesToScan.addAll(clazz.interfaceNames());
}
}
// Add reflective access to custom backoff strategies
for (ClassInfo strategy : index.getAllKnownImplementors(DotNames.CUSTOM_BACKOFF_STRATEGY)) {
reflectiveClass.produce(ReflectiveClassBuildItem.builder(strategy.name().toString()).methods().build());
Expand Down Expand Up @@ -217,6 +178,7 @@ public void transform(TransformationContext context) {
} else if (metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)) {
builder.addBeanClass("io.smallrye.faulttolerance.metrics.MicrometerProvider");
}
// TODO support for OpenTelemetry Metrics -- not present in Quarkus yet

beans.produce(builder.build());

Expand Down Expand Up @@ -270,6 +232,7 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
AnnotationProxyBuildItem annotationProxy,
BuildProducer<GeneratedClassBuildItem> generatedClasses,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<ReflectiveMethodBuildItem> reflectiveMethod,
BuildProducer<ValidationPhaseBuildItem.ValidationErrorBuildItem> errors,
BuildProducer<FaultToleranceInfoBuildItem> faultToleranceInfo) {

Expand All @@ -293,7 +256,7 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, false);

FaultToleranceScanner scanner = new FaultToleranceScanner(index, annotationStore, annotationProxy, classOutput,
recorderContext);
recorderContext, reflectiveMethod);

List<FaultToleranceMethod> ftMethods = new ArrayList<>();
List<Throwable> exceptions = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ public String retried() {
return counter + ":" + name;
}

@GET
@Path("/fallback")
public String fallback() {
AtomicInteger counter = new AtomicInteger();
String name = service.fallbackMethod(counter);
return counter + ":" + name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.faulttolerance.Fallback;
import org.eclipse.microprofile.faulttolerance.Retry;

import io.smallrye.faulttolerance.api.ApplyFaultTolerance;
Expand Down Expand Up @@ -36,4 +37,16 @@ public String retriedMethod(AtomicInteger counter) {
}
throw new MyFaultToleranceError();
}

@Fallback(fallbackMethod = "fallback")
public String fallbackMethod(AtomicInteger counter) {
if (counter.incrementAndGet() >= THRESHOLD) {
return name;
}
throw new IllegalArgumentException();
}

private String fallback(AtomicInteger counter) {
return "fallback";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class FaultToleranceTestCase {
URL uri;

@Test
public void testRetry() throws Exception {
public void test() throws Exception {
RestAssured
.given().baseUri(uri.toString())
.when().get()
Expand All @@ -30,5 +30,10 @@ public void testRetry() throws Exception {
.given().baseUri(uri.toString() + "/retried")
.when().get()
.then().body(is("2:Lucie"));

RestAssured
.given().baseUri(uri.toString() + "/fallback")
.when().get()
.then().body(is("1:fallback"));
}
}
12 changes: 11 additions & 1 deletion tcks/microprofile-fault-tolerance/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<name>Quarkus - TCK - MicroProfile Fault Tolerance</name>

<properties>
<microprofile-fault-tolerance-tck.version>4.0.2</microprofile-fault-tolerance-tck.version>
<microprofile-fault-tolerance-tck.version>4.1.1</microprofile-fault-tolerance-tck.version>
</properties>

<build>
Expand All @@ -35,6 +35,16 @@
</dependenciesToScan>
<reuseForks>false</reuseForks>
<excludes>
<!-- Quarkus doesn't implement OpenTelemetry Metrics yet -->
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.AllAnnotationTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.BulkheadTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.CircuitBreakerTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.ClashingNameTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.ClassLevelTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.FallbackTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.FaultToleranceDisabledTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.RetryTelemetryTest</exclude>
<exclude>org.eclipse.microprofile.fault.tolerance.tck.telemetryMetrics.TimeoutTelemetryTest</exclude>
<!-- We do not support enablement via beans.xml -->
<exclude>org.eclipse.microprofile.fault.tolerance.tck.interceptor.xmlInterceptorEnabling.FaultToleranceInterceptorEnableByXmlTest</exclude>
</excludes>
Expand Down

0 comments on commit 0a67472

Please sign in to comment.