diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 910a800db07c3..f618010642819 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -48,7 +48,7 @@ 2.1.7 1.2.8 2.0.0 - 5.1.0 + 5.2.0 3.2.0 1.2.0 1.0.13 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java index 18a9f9e103c7a..c79c8285ea7c4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/JandexUtil.java @@ -1,5 +1,6 @@ package io.quarkus.deployment.util; +import java.lang.reflect.Array; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; @@ -375,4 +376,56 @@ public ClassNotIndexedException(DotName dotName) { this.dotName = dotName; } } + + public static Class loadRawType(Type type) { + switch (type.kind()) { + case VOID: + return void.class; + case PRIMITIVE: + switch (type.asPrimitiveType().primitive()) { + case BOOLEAN: + return boolean.class; + case CHAR: + return char.class; + case BYTE: + return byte.class; + case SHORT: + return short.class; + case INT: + return int.class; + case LONG: + return long.class; + case FLOAT: + return float.class; + case DOUBLE: + return double.class; + default: + throw new IllegalArgumentException("Unknown primitive type: " + type); + } + case CLASS: + return load(type.asClassType().name()); + case PARAMETERIZED_TYPE: + return load(type.asParameterizedType().name()); + case ARRAY: + Class component = loadRawType(type.asArrayType().component()); + int dimensions = type.asArrayType().dimensions(); + return Array.newInstance(component, new int[dimensions]).getClass(); + case WILDCARD_TYPE: + return loadRawType(type.asWildcardType().extendsBound()); + case TYPE_VARIABLE: + return load(type.asTypeVariable().name()); + case UNRESOLVED_TYPE_VARIABLE: + return Object.class; // can't do better here + default: + throw new IllegalArgumentException("Unknown type: " + type); + } + } + + private static Class load(DotName name) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(name.toString()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } } diff --git a/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java b/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java index e2b8a47b0130c..3b86dd1d18e84 100644 --- a/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java +++ b/core/deployment/src/test/java/io/quarkus/deployment/util/JandexUtilTest.java @@ -2,13 +2,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.Map; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.Index; import org.jboss.jandex.Indexer; @@ -302,6 +305,72 @@ public static class MultiBoundedRepo implements Repo { public static class ErasedRepo2 extends MultiBoundedRepo { } + @Test + public void testLoadRawType() { + Index index = index(TestMethods.class); + ClassInfo clazz = index.getClassByName(DotName.createSimple(TestMethods.class.getName())); + + assertEquals(void.class, JandexUtil.loadRawType(clazz.method("aaa").returnType())); + assertEquals(int.class, JandexUtil.loadRawType(clazz.method("bbb").returnType())); + assertEquals(String.class, JandexUtil.loadRawType(clazz.method("ccc").returnType())); + assertEquals(List.class, JandexUtil.loadRawType(clazz.method("ddd").returnType())); + assertEquals(String[][].class, JandexUtil.loadRawType(clazz.method("eee").returnType())); + assertEquals(Object.class, JandexUtil.loadRawType(clazz.method("fff").returnType())); + assertEquals(Number.class, JandexUtil.loadRawType(clazz.method("ggg").returnType())); + assertEquals(Number.class, JandexUtil.loadRawType(clazz.method("hhh").returnType())); + assertEquals(Comparable.class, JandexUtil.loadRawType(clazz.method("iii").returnType())); + assertEquals(Comparable.class, JandexUtil.loadRawType(clazz.method("jjj").returnType())); + assertEquals(Serializable.class, JandexUtil.loadRawType(clazz.method("kkk").returnType())); + assertEquals(Object.class, JandexUtil.loadRawType(clazz.method("lll").returnType() + .asParameterizedType().arguments().get(0))); + assertEquals(Number.class, JandexUtil.loadRawType(clazz.method("mmm").returnType() + .asParameterizedType().arguments().get(0))); + assertEquals(Object.class, JandexUtil.loadRawType(clazz.method("nnn").returnType() + .asParameterizedType().arguments().get(0))); + assertEquals(Object.class, JandexUtil.loadRawType(clazz.method("ooo").returnType() + .asParameterizedType().arguments().get(0))); + assertEquals(Number.class, JandexUtil.loadRawType(clazz.method("ppp").returnType() + .asParameterizedType().arguments().get(0))); + assertEquals(Object.class, JandexUtil.loadRawType(clazz.method("qqq").returnType() + .asParameterizedType().arguments().get(0))); + } + + public interface TestMethods { + void aaa(); + + int bbb(); + + String ccc(); + + List ddd(); + + String[][] eee(); + + X fff(); + + Y ggg(); + + > Y hhh(); + + > Y iii(); + + & Serializable> Y jjj(); + + > Y kkk(); + + List lll(); + + List mmm(); + + List nnn(); + + List ooo(); + + List ppp(); + + List qqq(); + } + private static Index index(Class... classes) { Indexer indexer = new Indexer(); for (Class clazz : classes) { diff --git a/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc b/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc index 0e89e240f5da1..9626a1cb467d4 100644 --- a/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc +++ b/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc @@ -504,13 +504,13 @@ All that is needed to enable the fault tolerance features in Quarkus is: == Additional resources SmallRye Fault Tolerance has more features than shown here. -Please check the link:https://smallrye.io/docs/smallrye-fault-tolerance/5.1.0/index.html[SmallRye Fault Tolerance documentation] to learn about them. +Please check the link:https://smallrye.io/docs/smallrye-fault-tolerance/5.2.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. -Support for Mutiny is present, so your `@Asynchronous` methods can return `Uni`. +Support for Mutiny is present, so your asynchronous methods can return `Uni` in addition to `CompletionStage`. -MicroProfile Context Propagation is integrated with Fault Tolerance, so existing contexts are automatically propagated to your `@Asynchronous` methods. +MicroProfile Context Propagation is integrated with Fault Tolerance, so existing contexts are automatically propagated to your asynchronous methods. [NOTE] ==== @@ -520,3 +520,16 @@ This is contrary to MicroProfile Fault Tolerance specification, which states tha We believe that in presence of MicroProfile Context Propagation, this requirement should not apply. The entire point of context propagation is to make sure the new thread has the same contexts as the original thread. ==== + +Non-compatible mode is enabled by default, so methods that return `CompletionStage` (or `Uni`) have asynchronous fault tolerance applied without any `@Asynchronous`, `@Blocking` or `@NonBlocking` annotation. + +[NOTE] +==== +This mode is not compatible with the MicroProfile Fault Tolerance specification, albeit the incompatibility is very small. +To restore full compatibility, add this configuration property: + +[source,properties] +---- +smallrye.faulttolerance.mp-compatibility=true +---- +==== diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/DotNames.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/DotNames.java new file mode 100644 index 0000000000000..a5539f818e695 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/DotNames.java @@ -0,0 +1,46 @@ +package io.quarkus.smallrye.faulttolerance.deployment; + +import java.util.Set; + +import org.eclipse.microprofile.faulttolerance.Asynchronous; +import org.eclipse.microprofile.faulttolerance.Bulkhead; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; +import org.jboss.jandex.DotName; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.faulttolerance.api.CircuitBreakerName; +import io.smallrye.faulttolerance.api.CustomBackoff; +import io.smallrye.faulttolerance.api.CustomBackoffStrategy; +import io.smallrye.faulttolerance.api.ExponentialBackoff; +import io.smallrye.faulttolerance.api.FibonacciBackoff; + +public final class DotNames { + public static final DotName OBJECT = DotName.createSimple(Object.class.getName()); + + public static final DotName ASYNCHRONOUS = DotName.createSimple(Asynchronous.class.getName()); + public static final DotName BULKHEAD = DotName.createSimple(Bulkhead.class.getName()); + public static final DotName CIRCUIT_BREAKER = DotName.createSimple(CircuitBreaker.class.getName()); + public static final DotName FALLBACK = DotName.createSimple(Fallback.class.getName()); + public static final DotName RETRY = DotName.createSimple(Retry.class.getName()); + public static final DotName TIMEOUT = DotName.createSimple(Timeout.class.getName()); + + // SmallRye annotations (@CircuitBreakerName, @[Non]Blocking, @*Backoff) alone do _not_ trigger + // the fault tolerance interceptor, only in combination with other fault tolerance annotations + public static final Set FT_ANNOTATIONS = Set.of(ASYNCHRONOUS, BULKHEAD, CIRCUIT_BREAKER, FALLBACK, RETRY, TIMEOUT); + + public static final DotName BLOCKING = DotName.createSimple(Blocking.class.getName()); + public static final DotName NON_BLOCKING = DotName.createSimple(NonBlocking.class.getName()); + + public static final DotName CIRCUIT_BREAKER_NAME = DotName.createSimple(CircuitBreakerName.class.getName()); + + public static final DotName EXPONENTIAL_BACKOFF = DotName.createSimple(ExponentialBackoff.class.getName()); + public static final DotName FIBONACCI_BACKOFF = DotName.createSimple(FibonacciBackoff.class.getName()); + public static final DotName CUSTOM_BACKOFF = DotName.createSimple(CustomBackoff.class.getName()); + public static final DotName CUSTOM_BACKOFF_STRATEGY = DotName.createSimple(CustomBackoffStrategy.class.getName()); + + public static final Set BACKOFF_ANNOTATIONS = Set.of(EXPONENTIAL_BACKOFF, FIBONACCI_BACKOFF, CUSTOM_BACKOFF); +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java new file mode 100644 index 0000000000000..e787009138a8e --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java @@ -0,0 +1,181 @@ +package io.quarkus.smallrye.faulttolerance.deployment; + +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +import org.eclipse.microprofile.faulttolerance.Asynchronous; +import org.eclipse.microprofile.faulttolerance.Bulkhead; +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.arc.processor.AnnotationStore; +import io.quarkus.deployment.builditem.AnnotationProxyBuildItem; +import io.quarkus.deployment.util.JandexUtil; +import io.quarkus.gizmo.ClassOutput; +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; +import io.smallrye.faulttolerance.api.CircuitBreakerName; +import io.smallrye.faulttolerance.api.CustomBackoff; +import io.smallrye.faulttolerance.api.ExponentialBackoff; +import io.smallrye.faulttolerance.api.FibonacciBackoff; +import io.smallrye.faulttolerance.autoconfig.FaultToleranceMethod; +import io.smallrye.faulttolerance.autoconfig.MethodDescriptor; + +final class FaultToleranceScanner { + private final IndexView index; + private final AnnotationStore annotationStore; + + private final AnnotationProxyBuildItem proxy; + private final ClassOutput output; + + FaultToleranceScanner(IndexView index, AnnotationStore annotationStore, AnnotationProxyBuildItem proxy, + ClassOutput output) { + this.index = index; + this.annotationStore = annotationStore; + this.proxy = proxy; + this.output = output; + } + + boolean hasFTAnnotations(ClassInfo clazz) { + // first check annotations on type + if (annotationStore.hasAnyAnnotation(clazz, DotNames.FT_ANNOTATIONS)) { + return true; + } + + // then check on the methods + for (MethodInfo method : clazz.methods()) { + if (annotationStore.hasAnyAnnotation(method, DotNames.FT_ANNOTATIONS)) { + return true; + } + } + + // then check on the parent + DotName parentClassName = clazz.superName(); + if (parentClassName == null || parentClassName.equals(DotNames.OBJECT)) { + return false; + } + ClassInfo parentClass = index.getClassByName(parentClassName); + if (parentClass == null) { + return false; + } + return hasFTAnnotations(parentClass); + } + + void forEachMethod(ClassInfo clazz, Consumer action) { + for (MethodInfo method : clazz.methods()) { + if (method.name().startsWith("<")) { + // constructors (or static init blocks) can't be intercepted + continue; + } + if (method.isSynthetic()) { + // synthetic methods can't be intercepted + continue; + } + + action.accept(method); + } + + DotName parentClassName = clazz.superName(); + if (parentClassName == null || parentClassName.equals(DotNames.OBJECT)) { + return; + } + ClassInfo parentClass = index.getClassByName(parentClassName); + if (parentClass == null) { + return; + } + forEachMethod(parentClass, action); + } + + FaultToleranceMethod createFaultToleranceMethod(ClassInfo beanClass, MethodInfo method) { + Set> annotationsPresentDirectly = new HashSet<>(); + + FaultToleranceMethod result = new FaultToleranceMethod(); + + result.beanClass = load(beanClass.name()); + result.method = createMethodDescriptor(method); + + result.asynchronous = getAnnotation(Asynchronous.class, method, beanClass, annotationsPresentDirectly); + result.bulkhead = getAnnotation(Bulkhead.class, method, beanClass, annotationsPresentDirectly); + result.circuitBreaker = getAnnotation(CircuitBreaker.class, method, beanClass, annotationsPresentDirectly); + result.fallback = getAnnotation(Fallback.class, method, beanClass, annotationsPresentDirectly); + result.retry = getAnnotation(Retry.class, method, beanClass, annotationsPresentDirectly); + result.timeout = getAnnotation(Timeout.class, method, beanClass, annotationsPresentDirectly); + + result.circuitBreakerName = getAnnotation(CircuitBreakerName.class, method, beanClass, annotationsPresentDirectly); + result.customBackoff = getAnnotation(CustomBackoff.class, method, beanClass, annotationsPresentDirectly); + result.exponentialBackoff = getAnnotation(ExponentialBackoff.class, method, beanClass, annotationsPresentDirectly); + result.fibonacciBackoff = getAnnotation(FibonacciBackoff.class, method, beanClass, annotationsPresentDirectly); + + result.blocking = getAnnotation(Blocking.class, method, beanClass, annotationsPresentDirectly); + result.nonBlocking = getAnnotation(NonBlocking.class, method, beanClass, annotationsPresentDirectly); + + result.annotationsPresentDirectly = annotationsPresentDirectly; + + return result; + } + + private MethodDescriptor createMethodDescriptor(MethodInfo method) { + MethodDescriptor result = new MethodDescriptor(); + result.declaringClass = load(method.declaringClass().name()); + result.name = method.name(); + result.parameterTypes = method.parameters() + .stream() + .map(JandexUtil::loadRawType) + .toArray(Class[]::new); + result.returnType = JandexUtil.loadRawType(method.returnType()); + return result; + } + + private A getAnnotation(Class annotationType, MethodInfo method, + ClassInfo beanClass, Set> directlyPresent) { + + DotName annotationName = DotName.createSimple(annotationType.getName()); + if (annotationStore.hasAnnotation(method, annotationName)) { + directlyPresent.add(annotationType); + AnnotationInstance annotation = annotationStore.getAnnotation(method, annotationName); + return createAnnotation(annotationType, annotation); + } + + return getAnnotationFromClass(annotationType, beanClass); + } + + private A getAnnotationFromClass(Class annotationType, ClassInfo clazz) { + DotName annotationName = DotName.createSimple(annotationType.getName()); + if (annotationStore.hasAnnotation(clazz, annotationName)) { + AnnotationInstance annotation = annotationStore.getAnnotation(clazz, annotationName); + return createAnnotation(annotationType, annotation); + } + + // then check on the parent + DotName parentClassName = clazz.superName(); + if (parentClassName == null || parentClassName.equals(DotNames.OBJECT)) { + return null; + } + ClassInfo parentClass = index.getClassByName(parentClassName); + if (parentClass == null) { + return null; + } + return getAnnotationFromClass(annotationType, parentClass); + } + + private A createAnnotation(Class annotationType, AnnotationInstance instance) { + return proxy.builder(instance, annotationType).build(output); + } + + private static Class load(DotName name) { + try { + return Thread.currentThread().getContextClassLoader().loadClass(name.toString()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java index bdb9d6a90a53b..0f8b969f90ccc 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java @@ -17,13 +17,7 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; -import org.eclipse.microprofile.faulttolerance.Asynchronous; -import org.eclipse.microprofile.faulttolerance.Bulkhead; -import org.eclipse.microprofile.faulttolerance.CircuitBreaker; -import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.FallbackHandler; -import org.eclipse.microprofile.faulttolerance.Retry; -import org.eclipse.microprofile.faulttolerance.Timeout; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationTarget.Kind; @@ -43,20 +37,24 @@ import io.quarkus.arc.processor.BeanInfo; import io.quarkus.arc.processor.BuildExtension; import io.quarkus.arc.processor.BuiltinScope; -import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; +import io.quarkus.deployment.builditem.AnnotationProxyBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ConfigurationTypeBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; +import io.quarkus.gizmo.ClassOutput; import io.quarkus.smallrye.faulttolerance.runtime.QuarkusAsyncExecutorProvider; import io.quarkus.smallrye.faulttolerance.runtime.QuarkusExistingCircuitBreakerNames; import io.quarkus.smallrye.faulttolerance.runtime.QuarkusFallbackHandlerProvider; @@ -67,7 +65,8 @@ import io.smallrye.faulttolerance.FaultToleranceBinding; import io.smallrye.faulttolerance.FaultToleranceInterceptor; import io.smallrye.faulttolerance.RequestContextIntegration; -import io.smallrye.faulttolerance.api.CircuitBreakerName; +import io.smallrye.faulttolerance.SpecCompatibility; +import io.smallrye.faulttolerance.autoconfig.FaultToleranceMethod; import io.smallrye.faulttolerance.core.util.RunnableWrapper; import io.smallrye.faulttolerance.internal.RequestContextControllerProvider; import io.smallrye.faulttolerance.internal.StrategyCache; @@ -77,18 +76,6 @@ public class SmallRyeFaultToleranceProcessor { - private static final Set FT_ANNOTATIONS = new HashSet<>(); - static { - // @Blocking and @NonBlocking alone do _not_ trigger the fault tolerance interceptor, - // only in combination with other fault tolerance annotations - FT_ANNOTATIONS.add(DotName.createSimple(Asynchronous.class.getName())); - FT_ANNOTATIONS.add(DotName.createSimple(Bulkhead.class.getName())); - FT_ANNOTATIONS.add(DotName.createSimple(CircuitBreaker.class.getName())); - FT_ANNOTATIONS.add(DotName.createSimple(Fallback.class.getName())); - FT_ANNOTATIONS.add(DotName.createSimple(Retry.class.getName())); - FT_ANNOTATIONS.add(DotName.createSimple(Timeout.class.getName())); - } - @BuildStep public void build(BuildProducer annotationsTransformer, BuildProducer feature, BuildProducer additionalBean, @@ -98,7 +85,8 @@ public void build(BuildProducer annotationsTran BuildProducer systemProperty, CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer reflectiveClass, - BuildProducer reflectiveMethod) { + BuildProducer reflectiveMethod, + BuildProducer config) { feature.produce(new FeatureBuildItem(Feature.SMALLRYE_FAULT_TOLERANCE)); @@ -125,7 +113,7 @@ public void build(BuildProducer annotationsTran additionalBean.produce(fallbackHandlersBeans.build()); } // Add reflective access to fallback methods - for (AnnotationInstance annotation : index.getAnnotations(DotName.createSimple(Fallback.class.getName()))) { + for (AnnotationInstance annotation : index.getAnnotations(DotNames.FALLBACK)) { AnnotationValue fallbackMethodValue = annotation.value("fallbackMethod"); if (fallbackMethodValue == null) { continue; @@ -161,8 +149,12 @@ public void build(BuildProducer annotationsTran classesToScan.addAll(clazz.interfaceNames()); } } + // Add reflective access to custom backoff strategies + for (ClassInfo strategy : index.getAllKnownImplementors(DotNames.CUSTOM_BACKOFF_STRATEGY)) { + reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, strategy.name().toString())); + } - for (DotName annotation : FT_ANNOTATIONS) { + for (DotName annotation : DotNames.FT_ANNOTATIONS) { reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, annotation.toString())); // also make them bean defining annotations additionalBda.produce(new BeanDefiningAnnotationBuildItem(annotation)); @@ -177,7 +169,7 @@ public boolean appliesTo(Kind kind) { @Override public void transform(TransformationContext context) { - if (FT_ANNOTATIONS.contains(context.getTarget().asClass().name())) { + if (DotNames.FT_ANNOTATIONS.contains(context.getTarget().asClass().name())) { context.transform().add(FaultToleranceBinding.class).done(); } } @@ -187,7 +179,7 @@ public void transform(TransformationContext context) { AdditionalBeanBuildItem.Builder builder = AdditionalBeanBuildItem.builder(); // Also register MP FT annotations so that they are recognized as interceptor bindings // Note that MP FT API jar is nor indexed, nor contains beans.xml so it is not part of the app index - for (DotName ftAnnotation : FT_ANNOTATIONS) { + for (DotName ftAnnotation : DotNames.FT_ANNOTATIONS) { builder.addBeanClass(ftAnnotation.toString()); } builder.addBeanClasses(FaultToleranceInterceptor.class, @@ -199,13 +191,16 @@ public void transform(TransformationContext context) { QuarkusAsyncExecutorProvider.class, MetricsProvider.class, CircuitBreakerMaintenanceImpl.class, - RequestContextIntegration.class); + RequestContextIntegration.class, + SpecCompatibility.class); additionalBean.produce(builder.build()); if (!metricsCapability.isPresent()) { //disable fault tolerance metrics with the MP sys props systemProperty.produce(new SystemPropertyBuildItem("MP_Fault_Tolerance_Metrics_Enabled", "false")); } + + config.produce(new RunTimeConfigurationDefaultBuildItem("smallrye.faulttolerance.mp-compatibility", "false")); } @BuildStep @@ -243,73 +238,81 @@ public void transform(TransformationContext ctx) { void validateFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, ValidationPhaseBuildItem validationPhase, BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + AnnotationProxyBuildItem annotationProxy, + BuildProducer generatedClasses, BuildProducer errors) { + AnnotationStore annotationStore = validationPhase.getContext().get(BuildExtension.Key.ANNOTATION_STORE); - Set beanNames = new HashSet<>(); IndexView index = beanArchiveIndexBuildItem.getIndex(); + // only generating annotation literal classes for MicroProfile/SmallRye Fault Tolerance annotations, + // none of them are application classes + ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, false); + + FaultToleranceScanner scaner = new FaultToleranceScanner(index, annotationStore, annotationProxy, classOutput); + + List ftMethods = new ArrayList<>(); + List exceptions = new ArrayList<>(); for (BeanInfo info : validationPhase.getContext().beans()) { - if (hasFTAnnotations(index, annotationStore, info.getImplClazz())) { - beanNames.add(info.getBeanClass().toString()); + ClassInfo beanClass = info.getImplClazz(); + if (beanClass == null) { + continue; } - } - recorder.createFaultToleranceOperation(beanNames); + if (scaner.hasFTAnnotations(beanClass)) { + scaner.forEachMethod(beanClass, method -> { + FaultToleranceMethod ftMethod = scaner.createFaultToleranceMethod(beanClass, method); + if (ftMethod.isLegitimate()) { + ftMethods.add(ftMethod); + + if (method.hasAnnotation(DotNames.BLOCKING) && method.hasAnnotation(DotNames.NON_BLOCKING)) { + exceptions.add( + new DefinitionException("Both @Blocking and @NonBlocking present on '" + method + "'")); + } + } + }); + + if (beanClass.classAnnotation(DotNames.BLOCKING) != null + && beanClass.classAnnotation(DotNames.NON_BLOCKING) != null) { + exceptions.add(new DefinitionException("Both @Blocking and @NonBlocking present on '" + beanClass + "'")); + } + } + } - DotName circuitBreakerName = DotName.createSimple(CircuitBreakerName.class.getName()); + recorder.createFaultToleranceOperation(ftMethods); Map> existingCircuitBreakerNames = new HashMap<>(); - for (AnnotationInstance it : index.getAnnotations(circuitBreakerName)) { + for (AnnotationInstance it : index.getAnnotations(DotNames.CIRCUIT_BREAKER_NAME)) { if (it.target().kind() == Kind.METHOD) { MethodInfo method = it.target().asMethod(); existingCircuitBreakerNames.computeIfAbsent(it.value().asString(), ignored -> new HashSet<>()) .add(method + " @ " + method.declaringClass()); } } - - List exceptions = new ArrayList<>(); for (Map.Entry> entry : existingCircuitBreakerNames.entrySet()) { if (entry.getValue().size() > 1) { exceptions.add(new DefinitionException("Multiple circuit breakers have the same name '" + entry.getKey() + "': " + entry.getValue())); } } - if (!exceptions.isEmpty()) { - errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(exceptions)); - } - recorder.initExistingCircuitBreakerNames(existingCircuitBreakerNames.keySet()); - } - - private boolean hasFTAnnotations(IndexView index, AnnotationStore annotationStore, ClassInfo info) { - if (info == null) { - //should not happen, but guard against it - //happens in this case due to a bug involving array types - - return false; - } - // first check annotations on type - if (annotationStore.hasAnyAnnotation(info, FT_ANNOTATIONS)) { - return true; - } - - // then check on the methods - for (MethodInfo method : info.methods()) { - if (annotationStore.hasAnyAnnotation(method, FT_ANNOTATIONS)) { - return true; + for (DotName backoffAnnotation : DotNames.BACKOFF_ANNOTATIONS) { + for (AnnotationInstance it : index.getAnnotations(backoffAnnotation)) { + if (it.target().kind() == Kind.CLASS && it.target().asClass().classAnnotation(DotNames.RETRY) == null) { + exceptions.add(new DefinitionException("Backoff annotation @" + backoffAnnotation.withoutPackagePrefix() + + " present on '" + it.target() + "', but @Retry is missing")); + } else if (it.target().kind() == Kind.METHOD && !it.target().asMethod().hasAnnotation(DotNames.RETRY)) { + exceptions.add(new DefinitionException("Backoff annotation @" + backoffAnnotation.withoutPackagePrefix() + + " present on '" + it.target() + "', but @Retry is missing")); + } } } - // then check on the parent - DotName parentClassName = info.superName(); - if (parentClassName == null || parentClassName.equals(DotNames.OBJECT)) { - return false; - } - ClassInfo parentClassInfo = index.getClassByName(parentClassName); - if (parentClassInfo == null) { - return false; + if (!exceptions.isEmpty()) { + errors.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(exceptions)); } - return hasFTAnnotations(index, annotationStore, parentClassInfo); + + recorder.initExistingCircuitBreakerNames(existingCircuitBreakerNames.keySet()); } @BuildStep diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnClassService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnClassService.java new file mode 100644 index 0000000000000..d4c7c60cc709a --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnClassService.java @@ -0,0 +1,20 @@ +package io.quarkus.smallrye.faulttolerance.test.asynchronous.additional; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; + +@ApplicationScoped +@Blocking +@NonBlocking +public class BlockingNonBlockingOnClassService { + @Retry + public CompletionStage hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnClassTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnClassTest.java new file mode 100644 index 0000000000000..9904ec64ce31c --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnClassTest.java @@ -0,0 +1,30 @@ +package io.quarkus.smallrye.faulttolerance.test.asynchronous.additional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DefinitionException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class BlockingNonBlockingOnClassTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(BlockingNonBlockingOnClassService.class)) + .assertException(e -> { + assertEquals(DefinitionException.class, e.getClass()); + assertTrue(e.getMessage().contains("Both @Blocking and @NonBlocking present")); + }); + + @Test + public void test() { + fail(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnMethodService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnMethodService.java new file mode 100644 index 0000000000000..fe945d873b067 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnMethodService.java @@ -0,0 +1,20 @@ +package io.quarkus.smallrye.faulttolerance.test.asynchronous.additional; + +import java.util.concurrent.CompletionStage; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.common.annotation.Blocking; +import io.smallrye.common.annotation.NonBlocking; + +@ApplicationScoped +public class BlockingNonBlockingOnMethodService { + @Retry + @Blocking + @NonBlocking + public CompletionStage hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnMethodTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnMethodTest.java new file mode 100644 index 0000000000000..383c523220138 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/additional/BlockingNonBlockingOnMethodTest.java @@ -0,0 +1,30 @@ +package io.quarkus.smallrye.faulttolerance.test.asynchronous.additional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DefinitionException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class BlockingNonBlockingOnMethodTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(BlockingNonBlockingOnMethodService.class)) + .assertException(e -> { + assertEquals(DefinitionException.class, e.getClass()); + assertTrue(e.getMessage().contains("Both @Blocking and @NonBlocking present")); + }); + + @Test + public void test() { + fail(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatAsyncTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatAsyncTest.java new file mode 100644 index 0000000000000..1d44a741f056f --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatAsyncTest.java @@ -0,0 +1,47 @@ +package io.quarkus.smallrye.faulttolerance.test.asynchronous.noncompat; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.CompletionStage; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class NoncompatAsyncTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(NoncompatHelloService.class)); + + @Inject + NoncompatHelloService service; + + @Test + public void noThreadOffloadAndFallback() throws Exception { + Thread mainThread = Thread.currentThread(); + + CompletionStage future = service.hello(); + assertThat(future.toCompletableFuture().get()).isEqualTo("hello"); + + // no delay between retries, all executions happen on the same thread + // if there _was_ a delay, subsequent retries would be offloaded to another thread + assertThat(service.getHelloThreads()).allSatisfy(thread -> { + assertThat(thread).isSameAs(mainThread); + }); + assertThat(service.getHelloStackTraces()).allSatisfy(stackTrace -> { + assertThat(stackTrace).anySatisfy(frame -> { + assertThat(frame.getClassName()).contains("io.smallrye.faulttolerance.core"); + }); + }); + + // 1 initial execution + 3 retries + assertThat(service.getInvocationCounter()).hasValue(4); + + assertThat(service.getFallbackThread()).isSameAs(mainThread); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatHelloService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatHelloService.java new file mode 100644 index 0000000000000..264fa6fd4f375 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/asynchronous/noncompat/NoncompatHelloService.java @@ -0,0 +1,54 @@ +package io.quarkus.smallrye.faulttolerance.test.asynchronous.noncompat; + +import static io.smallrye.faulttolerance.core.util.CompletionStages.failedFuture; +import static java.util.concurrent.CompletableFuture.completedFuture; + +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Fallback; +import org.eclipse.microprofile.faulttolerance.Retry; + +@ApplicationScoped +@Retry(maxRetries = 3, delay = 0, jitter = 0) +public class NoncompatHelloService { + private final List helloThreads = new CopyOnWriteArrayList<>(); + private final List helloStackTraces = new CopyOnWriteArrayList<>(); + + private final AtomicInteger invocationCounter = new AtomicInteger(); + + private volatile Thread fallbackThread; + + @Fallback(fallbackMethod = "fallback") + public CompletionStage hello() { + invocationCounter.incrementAndGet(); + helloThreads.add(Thread.currentThread()); + helloStackTraces.add(new Throwable().getStackTrace()); + return failedFuture(new RuntimeException()); + } + + public CompletionStage fallback() { + fallbackThread = Thread.currentThread(); + return completedFuture("hello"); + } + + List getHelloThreads() { + return helloThreads; + } + + List getHelloStackTraces() { + return helloStackTraces; + } + + AtomicInteger getInvocationCounter() { + return invocationCounter; + } + + Thread getFallbackThread() { + return fallbackThread; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/ClassAndMethodBackoffService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/ClassAndMethodBackoffService.java new file mode 100644 index 0000000000000..42d90507219e7 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/ClassAndMethodBackoffService.java @@ -0,0 +1,18 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.ExponentialBackoff; +import io.smallrye.faulttolerance.api.FibonacciBackoff; + +@ApplicationScoped +@Retry +@ExponentialBackoff +public class ClassAndMethodBackoffService { + @FibonacciBackoff + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/ClassAndMethodBackoffTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/ClassAndMethodBackoffTest.java new file mode 100644 index 0000000000000..0f3a6bc35582f --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/ClassAndMethodBackoffTest.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DefinitionException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class ClassAndMethodBackoffTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ClassAndMethodBackoffService.class)) + .assertException(e -> { + assertEquals(DefinitionException.class, e.getClass()); + assertTrue(e.getMessage().contains("Backoff annotation")); + assertTrue(e.getMessage().contains("@Retry is missing")); + }); + + @Test + public void test() { + fail(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnClassBackoffOnMethodService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnClassBackoffOnMethodService.java new file mode 100644 index 0000000000000..15c795ff0bcd7 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnClassBackoffOnMethodService.java @@ -0,0 +1,16 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.ExponentialBackoff; + +@ApplicationScoped +@Retry +public class RetryOnClassBackoffOnMethodService { + @ExponentialBackoff + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnClassBackoffOnMethodTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnClassBackoffOnMethodTest.java new file mode 100644 index 0000000000000..1d20c9acadd3c --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnClassBackoffOnMethodTest.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DefinitionException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RetryOnClassBackoffOnMethodTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(RetryOnClassBackoffOnMethodService.class)) + .assertException(e -> { + assertEquals(DefinitionException.class, e.getClass()); + assertTrue(e.getMessage().contains("Backoff annotation")); + assertTrue(e.getMessage().contains("@Retry is missing")); + }); + + @Test + public void test() { + fail(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnMethodBackoffOnClassService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnMethodBackoffOnClassService.java new file mode 100644 index 0000000000000..fbcd7093d7746 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnMethodBackoffOnClassService.java @@ -0,0 +1,16 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.FibonacciBackoff; + +@ApplicationScoped +@FibonacciBackoff +public class RetryOnMethodBackoffOnClassService { + @Retry + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnMethodBackoffOnClassTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnMethodBackoffOnClassTest.java new file mode 100644 index 0000000000000..135991aa5b26a --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/RetryOnMethodBackoffOnClassTest.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DefinitionException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RetryOnMethodBackoffOnClassTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(RetryOnMethodBackoffOnClassService.class)) + .assertException(e -> { + assertEquals(DefinitionException.class, e.getClass()); + assertTrue(e.getMessage().contains("Backoff annotation")); + assertTrue(e.getMessage().contains("@Retry is missing")); + }); + + @Test + public void test() { + fail(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/TwoBackoffsOnMethodService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/TwoBackoffsOnMethodService.java new file mode 100644 index 0000000000000..45a58027ba6b4 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/TwoBackoffsOnMethodService.java @@ -0,0 +1,18 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +import io.smallrye.faulttolerance.api.ExponentialBackoff; +import io.smallrye.faulttolerance.api.FibonacciBackoff; + +@ApplicationScoped +public class TwoBackoffsOnMethodService { + @Retry + @ExponentialBackoff + @FibonacciBackoff + public void hello() { + throw new IllegalArgumentException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/TwoBackoffsOnMethodTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/TwoBackoffsOnMethodTest.java new file mode 100644 index 0000000000000..4c6e9cdb39f96 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/retry/backoff/TwoBackoffsOnMethodTest.java @@ -0,0 +1,30 @@ +package io.quarkus.smallrye.faulttolerance.test.retry.backoff; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.inject.spi.DeploymentException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class TwoBackoffsOnMethodTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TwoBackoffsOnMethodService.class)) + .assertException(e -> { + assertEquals(DeploymentException.class, e.getClass()); + assertTrue(e.getMessage().contains("More than one backoff defined")); + }); + + @Test + public void test() { + fail(); + } +} diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFallbackHandlerProvider.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFallbackHandlerProvider.java index 319f309f4ddb0..f807114ab8756 100644 --- a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFallbackHandlerProvider.java +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFallbackHandlerProvider.java @@ -11,7 +11,6 @@ import org.eclipse.microprofile.faulttolerance.FallbackHandler; import io.smallrye.faulttolerance.FallbackHandlerProvider; -import io.smallrye.faulttolerance.config.FallbackConfig; import io.smallrye.faulttolerance.config.FaultToleranceOperation; @Dependent @@ -30,7 +29,7 @@ public FallbackHandler get(FaultToleranceOperation operation) { @SuppressWarnings("unchecked") @Override public T handle(ExecutionContext context) { - Class> clazz = operation.getFallback().get(FallbackConfig.VALUE); + Class> clazz = operation.getFallback().value(); FallbackHandler fallbackHandlerInstance = (FallbackHandler) instance.select(clazz).get(); try { return fallbackHandlerInstance.handle(context); diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFaultToleranceOperationProvider.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFaultToleranceOperationProvider.java index ca6effaa750de..85b41aa7b784c 100644 --- a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFaultToleranceOperationProvider.java +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/QuarkusFaultToleranceOperationProvider.java @@ -13,6 +13,7 @@ import org.jboss.logging.Logger; import io.smallrye.faulttolerance.FaultToleranceOperationProvider; +import io.smallrye.faulttolerance.config.FaultToleranceMethods; import io.smallrye.faulttolerance.config.FaultToleranceOperation; @Singleton @@ -45,7 +46,7 @@ public FaultToleranceOperation get(Class beanClass, Method method) { private FaultToleranceOperation createAtRuntime(CacheKey key) { LOG.debugf("FaultToleranceOperation not found in the cache for %s creating it at runtime", key); - return FaultToleranceOperation.of(key.beanClass, key.method); + return FaultToleranceOperation.create(FaultToleranceMethods.create(key.beanClass, key.method)); } static class CacheKey { diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java index c6730f50f61a3..f1443fb32d4cf 100644 --- a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/SmallRyeFaultToleranceRecorder.java @@ -1,9 +1,7 @@ package io.quarkus.smallrye.faulttolerance.runtime; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -14,42 +12,29 @@ import io.quarkus.arc.Arc; import io.quarkus.runtime.annotations.Recorder; +import io.smallrye.faulttolerance.autoconfig.FaultToleranceMethod; import io.smallrye.faulttolerance.config.FaultToleranceOperation; @Recorder public class SmallRyeFaultToleranceRecorder { - public void createFaultToleranceOperation(Set beanNames) { + public void createFaultToleranceOperation(List ftMethods) { List allExceptions = new ArrayList<>(); Map operationCache = new HashMap<>( - beanNames.size()); - for (String beanName : beanNames) { + ftMethods.size()); + for (FaultToleranceMethod ftMethod : ftMethods) { + FaultToleranceOperation operation = FaultToleranceOperation.create(ftMethod); try { - ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); - if (classLoader == null) { - classLoader = SmallRyeFaultToleranceRecorder.class.getClassLoader(); - } - Class beanClass = Class.forName(beanName, true, classLoader); - - for (Method method : getAllMethods(beanClass)) { - FaultToleranceOperation operation = FaultToleranceOperation.of(beanClass, method); - if (operation.isLegitimate()) { - try { - operation.validate(); + operation.validate(); - // register the operation at validation time to avoid re-creating it at runtime - QuarkusFaultToleranceOperationProvider.CacheKey cacheKey = new QuarkusFaultToleranceOperationProvider.CacheKey( - beanClass, method); - operationCache.put(cacheKey, operation); - } catch (FaultToleranceDefinitionException e) { - allExceptions.add(e); - } - } - } - } catch (ClassNotFoundException e) { - // Ignore + QuarkusFaultToleranceOperationProvider.CacheKey cacheKey = new QuarkusFaultToleranceOperationProvider.CacheKey( + ftMethod.beanClass, ftMethod.method.reflect()); + operationCache.put(cacheKey, operation); + } catch (FaultToleranceDefinitionException | NoSuchMethodException e) { + allExceptions.add(e); } } + if (!allExceptions.isEmpty()) { if (allExceptions.size() == 1) { Throwable error = allExceptions.get(0); @@ -71,19 +56,8 @@ public void createFaultToleranceOperation(Set beanNames) { throw deploymentException; } } - Arc.container().instance(QuarkusFaultToleranceOperationProvider.class).get().init(operationCache); - } - private Set getAllMethods(Class beanClass) { - Set allMethods = new HashSet<>(); - Class currentClass = beanClass; - while (currentClass != null && !currentClass.equals(Object.class)) { - for (Method m : currentClass.getDeclaredMethods()) { - allMethods.add(m); - } - currentClass = currentClass.getSuperclass(); // this will be null for interfaces - } - return allMethods; + Arc.container().instance(QuarkusFaultToleranceOperationProvider.class).get().init(operationCache); } public void initExistingCircuitBreakerNames(Set names) { diff --git a/tcks/microprofile-fault-tolerance/pom.xml b/tcks/microprofile-fault-tolerance/pom.xml index 9d954b32e59f6..57109ab08dfcf 100644 --- a/tcks/microprofile-fault-tolerance/pom.xml +++ b/tcks/microprofile-fault-tolerance/pom.xml @@ -21,6 +21,7 @@ false + true @@ -32,6 +33,9 @@ org.eclipse.microprofile.fault.tolerance.tck.interceptor.xmlInterceptorEnabling.FaultToleranceInterceptorEnableByXmlTest + + org.eclipse.microprofile.fault.tolerance.tck.invalidParameters.InvalidCircuitBreakerFailureRatioNegTest + org.eclipse.microprofile.fault.tolerance.tck.invalidParameters.InvalidCircuitBreakerFailureRatioPosTest