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

Upgrade to Smallrye Fault Tolerance 6.5.0 and resolve FT methods during build #43833

Merged
merged 2 commits into from
Oct 14, 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
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
Original file line number Diff line number Diff line change
Expand Up @@ -186,21 +186,33 @@ public A build(ClassOutput classOutput) {
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "getAnnotationLiteralType":
return annotationLiteral;
case "getAnnotationClass":
return annotationClass;
case "getAnnotationInstance":
return annotationInstance;
case "getDefaultValues":
return defaultValues;
case "getValues":
return values;
default:
break;
}
throw new UnsupportedOperationException("Method " + method + " not implemented");
String name = method.getName();
return switch (name) {
case "getAnnotationLiteralType" -> annotationLiteral;
case "getAnnotationClass" -> annotationClass;
case "getAnnotationInstance" -> annotationInstance;
case "getDefaultValues" -> defaultValues;
case "getValues" -> values;
default -> {
MethodInfo member = annotationClass.firstMethod(name);
if (member != null) {
if (values.containsKey(name)) {
yield values.get(name);
}
if (annotationInstance.value(name) != null) {
yield annotationInstance.value(name).value();
}
if (defaultValues.containsKey(name)) {
yield defaultValues.get(name);
}
if (member.defaultValue() != null) {
yield member.defaultValue().value();
}
throw new UnsupportedOperationException("Unknown value of annotation member " + name);
}
throw new UnsupportedOperationException("Method " + method + " not implemented");
}
};
}
});
}
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"));
}
}
Loading
Loading