diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 81d8b114e0880..8b6b461b6b1ba 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -53,7 +53,7 @@ 4.0.0 4.0.3 2.11.0 - 6.6.3 + 6.7.0 4.6.1 2.1.2 1.0.13 diff --git a/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc b/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc index bec770f84c8a0..42c47a9d441ee 100644 --- a/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc +++ b/docs/src/main/asciidoc/smallrye-fault-tolerance.adoc @@ -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 <>. +The specification-defined configuration properties are still supported, but the Quarkus-native configuration takes priority. == Conclusion @@ -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. @@ -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] @@ -601,12 +604,12 @@ 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. @@ -614,3 +617,43 @@ 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 `""` below is: + +* `"/"` for per method configuration +* `""` 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 + +|`///` +|`quarkus.fault-tolerance."/"..` + +|`//` +|`quarkus.fault-tolerance.""..` + +|`/` +|`quarkus.fault-tolerance.global..` + +|`MP_Fault_Tolerance_NonFallback_Enabled` +|`quarkus.fault-tolerance.enabled` + +|`MP_Fault_Tolerance_Metrics_Enabled` +|`quarkus.fault-tolerance.metrics.enabled` +|=== + +All the `` and `` 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` diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/ConfigUtilJandex.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/ConfigUtilJandex.java new file mode 100644 index 0000000000000..f1acb8af6f4b7 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/ConfigUtilJandex.java @@ -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 annotation, String member, MethodInfo declaringMethod) { + return ConfigPrefix.VALUE + "\"" + declaringMethod.declaringClass().name() + "/" + declaringMethod.name() + + "\"." + NewConfig.get(annotation, member); + } + + static String oldKey(Class annotation, String member, MethodInfo declaringMethod) { + return declaringMethod.declaringClass().name() + "/" + declaringMethod.name() + + "/" + annotation.getSimpleName() + "/" + member; + } + + static String newKey(Class annotation, String member, ClassInfo declaringClass) { + return ConfigPrefix.VALUE + "\"" + declaringClass.name() + "\"." + + NewConfig.get(annotation, member); + } + + static String oldKey(Class annotation, String member, ClassInfo declaringClass) { + return declaringClass.name() + "/" + annotation.getSimpleName() + "/" + member; + } + + static String newKey(Class annotation, String member) { + return ConfigPrefix.VALUE + GLOBAL + "." + NewConfig.get(annotation, member); + } + + static String oldKey(Class annotation, String member) { + return annotation.getSimpleName() + "/" + member; + } + + // no need to have the `isEnabled()` method, that is only needed at runtime +} 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 index 556d0cd9c474a..012dc7f011320 100644 --- 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 @@ -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; @@ -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); @@ -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 FT_ANNOTATIONS = Set.of(APPLY_FAULT_TOLERANCE, ASYNCHRONOUS, + public static final Set 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 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 index c68cd65bce9f2..609952959b297 100644 --- 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 @@ -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; @@ -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, @@ -236,18 +238,30 @@ private String getMethodNameFromConfig(MethodInfo method, Set/// - String key = method.declaringClass().name() + "/" + method.name() + "/" + ftAnnotation.getSimpleName() + "/" - + memberName; - result = config.getOptionalValue(key, String.class).orElse(null); + // smallrye.faulttolerance."/".. + String newKey = ConfigUtilJandex.newKey(ftAnnotation, memberName, method); + // /// + String oldKey = ConfigUtilJandex.oldKey(ftAnnotation, memberName, method); + result = config.getOptionalValue(newKey, String.class) + .or(() -> config.getOptionalValue(oldKey, String.class)) + .orElse(null); } else { - // // - String key = method.declaringClass().name() + "/" + ftAnnotation.getSimpleName() + "/" + memberName; - result = config.getOptionalValue(key, String.class).orElse(null); + // smallrye.faulttolerance."".. + String newKey = ConfigUtilJandex.newKey(ftAnnotation, memberName, method.declaringClass()); + // // + 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) { - // / - result = config.getOptionalValue(ftAnnotation.getSimpleName() + "/" + memberName, String.class).orElse(null); + // smallrye.faulttolerance.global.. + String newKey = ConfigUtilJandex.newKey(ftAnnotation, memberName); + // / + String oldKey = ConfigUtilJandex.oldKey(ftAnnotation, memberName); + result = config.getOptionalValue(newKey, String.class) + .or(() -> config.getOptionalValue(oldKey, String.class)) + .orElse(null); } return result; } 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 1a27a6f88ce8e..c0b10c61f8b3c 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 @@ -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; @@ -94,6 +97,8 @@ public void build(BuildProducer 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())); @@ -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"); @@ -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")); @@ -262,7 +268,21 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, List exceptions = new ArrayList<>(); Map> existingCircuitBreakerNames = new HashMap<>(); + Map> existingGuards = new HashMap<>(); + Set 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; @@ -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()); + } } }); @@ -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()); + } } } @@ -353,6 +381,19 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder, } } + for (Map.Entry> 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)); } diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js b/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js index d05d2dc0b123c..e188bc1de398d 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/resources/dev-ui/qwc-fault-tolerance-methods.js @@ -72,6 +72,7 @@ export class QwcFaultToleranceMethods extends LitElement { return html` ${guardedMethod.ApplyFaultTolerance ? this._renderApplyFaultTolerance(guardedMethod.ApplyFaultTolerance) : html``} + ${guardedMethod.ApplyGuard ? this._renderApplyGuard(guardedMethod.ApplyGuard) : html``} ${guardedMethod.Asynchronous ? html`@Asynchronous` : html``} ${guardedMethod.AsynchronousNonBlocking ? html`@AsynchronousNonBlocking` : html``} ${guardedMethod.Blocking ? html`@Blocking` : html``} @@ -98,6 +99,12 @@ export class QwcFaultToleranceMethods extends LitElement { `; } + _renderApplyGuard(applyGuard) { + return html` + @ApplyGuard("${applyGuard.value}") + `; + } + _renderBulkhead(bulkhead) { return html` @Bulkhead(value = ${bulkhead.value}, waitingTaskQueue = ${bulkhead.waitingTaskQueue}) diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/BulkheadConfigBean.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/BulkheadConfigBean.java new file mode 100644 index 0000000000000..8cbc750ddad01 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/BulkheadConfigBean.java @@ -0,0 +1,26 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static java.util.concurrent.CompletableFuture.completedFuture; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Asynchronous; +import org.eclipse.microprofile.faulttolerance.Bulkhead; + +@ApplicationScoped +public class BulkheadConfigBean { + @Bulkhead(value = 5) + public void value(CompletableFuture barrier) { + barrier.join(); + } + + @Bulkhead(value = 1, waitingTaskQueue = 5) + @Asynchronous + public Future waitingTaskQueue(CompletableFuture barrier) { + barrier.join(); + return completedFuture(null); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/BulkheadConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/BulkheadConfigTest.java new file mode 100644 index 0000000000000..b1aa0b50e84e6 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/BulkheadConfigTest.java @@ -0,0 +1,74 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.faulttolerance.exceptions.BulkheadException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class BulkheadConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(BulkheadConfigBean.class)) + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.BulkheadConfigBean/value\".bulkhead.value", + "1") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.BulkheadConfigBean/waitingTaskQueue\".bulkhead.waiting-task-queue", + "1"); + + @Inject + private BulkheadConfigBean bean; + + private ExecutorService executor; + + @BeforeEach + public void setUp() { + executor = Executors.newCachedThreadPool(); + } + + @AfterEach + public void tearDown() throws InterruptedException { + executor.shutdownNow(); + executor.awaitTermination(1, TimeUnit.SECONDS); + } + + @Test + public void value() throws Exception { + CompletableFuture barrier = new CompletableFuture<>(); + + executor.submit(() -> bean.value(barrier)); + Thread.sleep(500); + assertThatThrownBy(() -> bean.value(null)).isExactlyInstanceOf(BulkheadException.class); + + barrier.complete(null); + } + + @Test + public void waitingTaskQueue() throws Exception { + CompletableFuture barrier1 = new CompletableFuture<>(); + CompletableFuture barrier2 = new CompletableFuture<>(); + + executor.submit(() -> bean.waitingTaskQueue(barrier1)); + executor.submit(() -> bean.waitingTaskQueue(barrier2)); + Thread.sleep(500); + assertThatThrownBy(() -> bean.waitingTaskQueue(null).get()) + .isExactlyInstanceOf(ExecutionException.class) + .hasCauseExactlyInstanceOf(BulkheadException.class); + + barrier1.complete(null); + barrier2.complete(null); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/CircuitBreakerConfigBean.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/CircuitBreakerConfigBean.java new file mode 100644 index 0000000000000..32b2887d090f3 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/CircuitBreakerConfigBean.java @@ -0,0 +1,46 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import java.time.temporal.ChronoUnit; + +import jakarta.enterprise.context.Dependent; + +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; + +@Dependent +public class CircuitBreakerConfigBean { + @CircuitBreaker(requestVolumeThreshold = 2, failOn = TestConfigExceptionB.class) + public void failOn() { + throw new TestConfigExceptionA(); + } + + @CircuitBreaker(requestVolumeThreshold = 2) + public void skipOn() { + throw new TestConfigExceptionA(); + } + + @CircuitBreaker(requestVolumeThreshold = 2, delay = 20, delayUnit = ChronoUnit.MICROS) + public void delay(boolean fail) { + if (fail) { + throw new TestConfigExceptionA(); + } + } + + @CircuitBreaker(requestVolumeThreshold = 2) + public void requestVolumeThreshold() { + throw new TestConfigExceptionA(); + } + + @CircuitBreaker(requestVolumeThreshold = 10, failureRatio = 1.0) + public void failureRatio(boolean fail) { + if (fail) { + throw new TestConfigExceptionA(); + } + } + + @CircuitBreaker(requestVolumeThreshold = 10, successThreshold = 4, delay = 1000) + public void successThreshold(boolean fail) { + if (fail) { + throw new TestConfigExceptionA(); + } + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/CircuitBreakerConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/CircuitBreakerConfigTest.java new file mode 100644 index 0000000000000..1ede02f0b69ed --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/CircuitBreakerConfigTest.java @@ -0,0 +1,136 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class CircuitBreakerConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(CircuitBreakerConfigBean.class, TestConfigExceptionA.class, + TestConfigExceptionB.class)) + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.CircuitBreakerConfigBean/skipOn\".circuit-breaker.skip-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.CircuitBreakerConfigBean/failOn\".circuit-breaker.fail-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.CircuitBreakerConfigBean/delay\".circuit-breaker.delay", + "1000") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.CircuitBreakerConfigBean/delay\".circuit-breaker.delay-unit", + "millis") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.CircuitBreakerConfigBean/requestVolumeThreshold\".circuit-breaker.request-volume-threshold", + "4") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.CircuitBreakerConfigBean/failureRatio\".circuit-breaker.failure-ratio", + "0.8") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.CircuitBreakerConfigBean/successThreshold\".circuit-breaker.success-threshold", + "2"); + + @Inject + private CircuitBreakerConfigBean bean; + + @Test + public void failOn() { + assertThatThrownBy(() -> bean.failOn()).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.failOn()).isExactlyInstanceOf(TestConfigExceptionA.class); + + assertThatThrownBy(() -> bean.failOn()).isExactlyInstanceOf(CircuitBreakerOpenException.class); + } + + @Test + public void testConfigureSkipOn() { + assertThatThrownBy(() -> bean.skipOn()).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.skipOn()).isExactlyInstanceOf(TestConfigExceptionA.class); + + assertThatThrownBy(() -> bean.skipOn()).isExactlyInstanceOf(TestConfigExceptionA.class); + } + + @Test + public void delay() { + assertThatThrownBy(() -> bean.delay(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.delay(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + + assertThatThrownBy(() -> bean.delay(false)).isExactlyInstanceOf(CircuitBreakerOpenException.class); + + long start = System.nanoTime(); + await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { + assertThatCode(() -> bean.delay(false)).doesNotThrowAnyException(); + }); + long end = System.nanoTime(); + + long durationInMillis = Duration.ofNanos(end - start).toMillis(); + assertThat(durationInMillis).isGreaterThan(800); + assertThat(durationInMillis).isLessThan(2000); + } + + @Test + public void requestVolumeThreshold() { + assertThatThrownBy(() -> bean.requestVolumeThreshold()).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.requestVolumeThreshold()).isExactlyInstanceOf(TestConfigExceptionA.class); + + assertThatThrownBy(() -> bean.requestVolumeThreshold()).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.requestVolumeThreshold()).isExactlyInstanceOf(TestConfigExceptionA.class); + + assertThatThrownBy(() -> bean.requestVolumeThreshold()).isExactlyInstanceOf(CircuitBreakerOpenException.class); + } + + @Test + public void failureRatio() { + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatCode(() -> bean.failureRatio(false)).doesNotThrowAnyException(); + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatCode(() -> bean.failureRatio(false)).doesNotThrowAnyException(); + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + assertThatThrownBy(() -> bean.failureRatio(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + + assertThatThrownBy(() -> bean.failureRatio(false)).isExactlyInstanceOf(CircuitBreakerOpenException.class); + } + + @Test + public void successThreshold() { + for (int i = 0; i < 10; i++) { + assertThatThrownBy(() -> bean.successThreshold(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + } + + assertThatThrownBy(() -> bean.successThreshold(false)).isExactlyInstanceOf(CircuitBreakerOpenException.class); + await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { + assertThatCode(() -> bean.successThreshold(false)).doesNotThrowAnyException(); + }); + + assertThatCode(() -> bean.successThreshold(false)).doesNotThrowAnyException(); + + for (int i = 0; i < 10; i++) { + assertThatThrownBy(() -> bean.successThreshold(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + } + + assertThatThrownBy(() -> bean.successThreshold(false)).isExactlyInstanceOf(CircuitBreakerOpenException.class); + await().atMost(1, TimeUnit.MINUTES).untilAsserted(() -> { + assertThatCode(() -> bean.successThreshold(false)).doesNotThrowAnyException(); + }); + + assertThatThrownBy(() -> bean.successThreshold(true)).isExactlyInstanceOf(TestConfigExceptionA.class); + + assertThatThrownBy(() -> bean.successThreshold(false)).isExactlyInstanceOf(CircuitBreakerOpenException.class); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyBean.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyBean.java new file mode 100644 index 0000000000000..2fe2cdc1e3086 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyBean.java @@ -0,0 +1,21 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import jakarta.enterprise.context.Dependent; + +import org.eclipse.microprofile.faulttolerance.Retry; + +@Dependent +@Retry +public class ConfigPropertyBean { + private int retry = 0; + + @Retry + public void triggerException() { + retry++; + throw new IllegalStateException("Exception"); + } + + public int getRetry() { + return retry; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyGlobalVsClassTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyGlobalVsClassTest.java new file mode 100644 index 0000000000000..f9062a917a609 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyGlobalVsClassTest.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigPropertyGlobalVsClassTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(ConfigPropertyBean.class)) + .overrideConfigKey("quarkus.fault-tolerance.global.retry.max-retries", + "7") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.ConfigPropertyBean\".retry.max-retries", + "5"); + + @Inject + private ConfigPropertyBean bean; + + @Test + void test() { + assertThatThrownBy(() -> bean.triggerException()).isExactlyInstanceOf(IllegalStateException.class); + assertThat(bean.getRetry()).isEqualTo(8); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyGlobalVsClassVsMethodTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyGlobalVsClassVsMethodTest.java new file mode 100644 index 0000000000000..89a34b6d9b727 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyGlobalVsClassVsMethodTest.java @@ -0,0 +1,33 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigPropertyGlobalVsClassVsMethodTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(ConfigPropertyBean.class)) + .overrideConfigKey("quarkus.fault-tolerance.global.retry.max-retries", "7") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.ConfigPropertyBean\".retry.max-retries", + "5") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.ConfigPropertyBean/triggerException\".retry.max-retries", + "6"); + + @Inject + private ConfigPropertyBean bean; + + @Test + void test() { + assertThatThrownBy(() -> bean.triggerException()).isExactlyInstanceOf(IllegalStateException.class); + assertThat(bean.getRetry()).isEqualTo(7); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyOnClassAndMethodTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyOnClassAndMethodTest.java new file mode 100644 index 0000000000000..49c19968b3814 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/ConfigPropertyOnClassAndMethodTest.java @@ -0,0 +1,32 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class ConfigPropertyOnClassAndMethodTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(ConfigPropertyBean.class)) + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.ConfigPropertyBean\".retry.max-retries", + "5") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.ConfigPropertyBean/triggerException\".retry.max-retries", + "6"); + + @Inject + private ConfigPropertyBean bean; + + @Test + void test() { + assertThatThrownBy(() -> bean.triggerException()).isExactlyInstanceOf(IllegalStateException.class); + assertThat(bean.getRetry()).isEqualTo(7); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackApplyOnConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackApplyOnConfigTest.java new file mode 100644 index 0000000000000..db27f32ebbff7 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackApplyOnConfigTest.java @@ -0,0 +1,27 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FallbackApplyOnConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(FallbackConfigBean.class, TestConfigExceptionA.class, + TestConfigExceptionB.class, FallbackHandlerA.class)) + .overrideConfigKey("quarkus.fault-tolerance.global.fallback.apply-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA"); + + @Inject + private FallbackConfigBean bean; + + @Test + public void applyOn() { + assertThat(bean.applyOn()).isEqualTo("FALLBACK"); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackConfigBean.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackConfigBean.java new file mode 100644 index 0000000000000..ac17ef1856315 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackConfigBean.java @@ -0,0 +1,36 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import jakarta.enterprise.context.Dependent; + +import org.eclipse.microprofile.faulttolerance.Fallback; + +@Dependent +public class FallbackConfigBean { + @Fallback(fallbackMethod = "theFallback", applyOn = TestConfigExceptionB.class) + public String applyOn() { + throw new TestConfigExceptionA(); + } + + @Fallback(fallbackMethod = "theFallback") + public String skipOn() { + throw new TestConfigExceptionA(); + } + + @Fallback(fallbackMethod = "theFallback") + public String fallbackMethod() { + throw new IllegalArgumentException(); + } + + @Fallback(FallbackHandlerA.class) + public String fallbackHandler() { + throw new IllegalArgumentException(); + } + + public String theFallback() { + return "FALLBACK"; + } + + public String anotherFallback() { + return "ANOTHER FALLBACK"; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackConfigTest.java new file mode 100644 index 0000000000000..7ecd1d62d8e22 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackConfigTest.java @@ -0,0 +1,53 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FallbackConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(FallbackConfigBean.class, TestConfigExceptionA.class, + TestConfigExceptionB.class, FallbackHandlerA.class, FallbackHandlerB.class)) + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.FallbackConfigBean/applyOn\".fallback.apply-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.FallbackConfigBean/skipOn\".fallback.skip-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.FallbackConfigBean/fallbackMethod\".fallback.fallback-method", + "anotherFallback") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.FallbackConfigBean/fallbackHandler\".fallback.value", + "io.quarkus.smallrye.faulttolerance.test.config.FallbackHandlerB"); + + @Inject + private FallbackConfigBean bean; + + @Test + public void applyOn() { + assertThat(bean.applyOn()).isEqualTo("FALLBACK"); + } + + @Test + public void skipOn() { + assertThatThrownBy(() -> bean.skipOn()).isExactlyInstanceOf(TestConfigExceptionA.class); + } + + @Test + public void fallbackMethod() { + assertThat(bean.fallbackMethod()).isEqualTo("ANOTHER FALLBACK"); + } + + @Test + public void fallbackHandler() { + assertThat(bean.fallbackHandler()).isEqualTo("FallbackHandlerB"); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackHandlerA.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackHandlerA.java new file mode 100644 index 0000000000000..8594a3d2d7ddb --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackHandlerA.java @@ -0,0 +1,11 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import org.eclipse.microprofile.faulttolerance.ExecutionContext; +import org.eclipse.microprofile.faulttolerance.FallbackHandler; + +public class FallbackHandlerA implements FallbackHandler { + @Override + public String handle(ExecutionContext context) { + return "FallbackHandlerA"; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackHandlerB.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackHandlerB.java new file mode 100644 index 0000000000000..c885264544e1b --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackHandlerB.java @@ -0,0 +1,11 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import org.eclipse.microprofile.faulttolerance.ExecutionContext; +import org.eclipse.microprofile.faulttolerance.FallbackHandler; + +public class FallbackHandlerB implements FallbackHandler { + @Override + public String handle(ExecutionContext context) { + return "FallbackHandlerB"; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackSkipOnConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackSkipOnConfigTest.java new file mode 100644 index 0000000000000..d7ef69846376b --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/FallbackSkipOnConfigTest.java @@ -0,0 +1,27 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FallbackSkipOnConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(FallbackConfigBean.class, TestConfigExceptionA.class, + TestConfigExceptionB.class, FallbackHandlerA.class)) + .overrideConfigKey("quarkus.fault-tolerance.global.fallback.skip-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA"); + + @Inject + private FallbackConfigBean bean; + + @Test + public void skipOn() { + assertThatThrownBy(() -> bean.skipOn()).isExactlyInstanceOf(TestConfigExceptionA.class); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RateLimitConfigBean.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RateLimitConfigBean.java new file mode 100644 index 0000000000000..b9dbf7d5c280d --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RateLimitConfigBean.java @@ -0,0 +1,25 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import java.time.temporal.ChronoUnit; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.faulttolerance.api.RateLimit; + +@ApplicationScoped +public class RateLimitConfigBean { + @RateLimit(value = 10) + public String value() { + return "value"; + } + + @RateLimit(value = 3, window = 10, windowUnit = ChronoUnit.MINUTES) + public String window() { + return "window"; + } + + @RateLimit(value = 3, minSpacing = 10, minSpacingUnit = ChronoUnit.MINUTES) + public String minSpacing() { + return "minSpacing"; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RateLimitConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RateLimitConfigTest.java new file mode 100644 index 0000000000000..06b5f7eaf92fd --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RateLimitConfigTest.java @@ -0,0 +1,66 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.faulttolerance.api.RateLimitException; + +public class RateLimitConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(RateLimitConfigBean.class)) + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RateLimitConfigBean/value\".rate-limit.value", + "3") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RateLimitConfigBean/window\".rate-limit.window", + "100") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RateLimitConfigBean/window\".rate-limit.window-unit", + "millis") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RateLimitConfigBean/minSpacing\".rate-limit.min-spacing", + "100") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RateLimitConfigBean/minSpacing\".rate-limit.min-spacing-unit", + "millis"); + + @Inject + private RateLimitConfigBean bean; + + @Test + public void value() throws Exception { + for (int i = 0; i < 3; i++) { + assertThat(bean.value()).isEqualTo("value"); + } + assertThatThrownBy(() -> bean.value()).isExactlyInstanceOf(RateLimitException.class); + } + + @Test + public void window() throws Exception { + for (int i = 0; i < 3; i++) { + assertThat(bean.window()).isEqualTo("window"); + } + assertThatThrownBy(() -> bean.window()).isExactlyInstanceOf(RateLimitException.class); + + Thread.sleep(500); + + assertThat(bean.window()).isEqualTo("window"); + } + + @Test + public void minSpacing() throws Exception { + assertThat(bean.minSpacing()).isEqualTo("minSpacing"); + assertThatThrownBy(() -> bean.minSpacing()).isExactlyInstanceOf(RateLimitException.class); + + Thread.sleep(500); + + assertThat(bean.minSpacing()).isEqualTo("minSpacing"); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RetryConfigBean.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RetryConfigBean.java new file mode 100644 index 0000000000000..e31e52ebdfdbb --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RetryConfigBean.java @@ -0,0 +1,56 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Retry; + +@ApplicationScoped +public class RetryConfigBean { + @Retry(delay = 0, jitter = 0) + public void maxRetries(AtomicInteger counter) { + counter.getAndIncrement(); + throw new TestException(); + } + + @Retry(maxDuration = 10000, durationUnit = ChronoUnit.MILLIS, maxRetries = 10000, delay = 200, jitter = 0) + public void maxDuration() { + throw new TestException(); + } + + @Retry(maxRetries = 5, delay = 2, delayUnit = ChronoUnit.SECONDS, jitter = 0) + public void delay() { + throw new TestException(); + } + + @Retry(maxRetries = 1, delay = 0, jitter = 0) + public void retryOn(RuntimeException e, AtomicInteger counter) { + counter.getAndIncrement(); + throw e; + } + + @Retry(retryOn = { TestConfigExceptionA.class, + TestConfigExceptionB.class }, abortOn = RuntimeException.class, maxRetries = 1, delay = 0, jitter = 0) + public void abortOn(RuntimeException e, AtomicInteger counter) { + counter.getAndIncrement(); + throw e; + } + + private long lastStartTime = 0; + + @Retry(abortOn = TestConfigExceptionA.class, delay = 0, jitter = 0, maxRetries = 1000, maxDuration = 10, durationUnit = ChronoUnit.SECONDS) + public void jitter() { + long startTime = System.nanoTime(); + if (lastStartTime != 0) { + Duration delay = Duration.ofNanos(startTime - lastStartTime); + if (delay.compareTo(Duration.ofMillis(100)) > 0) { + throw new TestConfigExceptionA(); + } + } + lastStartTime = startTime; + throw new TestException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RetryConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RetryConfigTest.java new file mode 100644 index 0000000000000..103e187c1969d --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/RetryConfigTest.java @@ -0,0 +1,131 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class RetryConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(RetryConfigBean.class, TestException.class, + TestConfigExceptionA.class, TestConfigExceptionB.class, TestConfigExceptionB1.class)) + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/maxRetries\".retry.max-retries", + "10") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/maxDuration\".retry.max-duration", + "1") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/maxDuration\".retry.max-duration-unit", + "seconds") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/delay\".retry.delay", + "2000") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/delay\".retry.delay-unit", + "micros") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/retryOn\".retry.retry-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA,io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionB") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/abortOn\".retry.abort-on", + "io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionA,io.quarkus.smallrye.faulttolerance.test.config.TestConfigExceptionB1") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/jitter\".retry.jitter", + "1") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.RetryConfigBean/jitter\".retry.jitter-unit", + "seconds"); + + @Inject + private RetryConfigBean bean; + + @Test + public void maxRetries() { + AtomicInteger counter = new AtomicInteger(); + assertThatThrownBy(() -> bean.maxRetries(counter)).isExactlyInstanceOf(TestException.class); + assertThat(counter).hasValue(11); + } + + @Test + public void maxDuration() { + long startTime = System.nanoTime(); + assertThatThrownBy(() -> bean.maxDuration()).isExactlyInstanceOf(TestException.class); + long endTime = System.nanoTime(); + + Duration duration = Duration.ofNanos(endTime - startTime); + assertThat(duration).isLessThan(Duration.ofSeconds(8)); + } + + @Test + public void delay() { + long startTime = System.nanoTime(); + assertThatThrownBy(() -> bean.delay()).isExactlyInstanceOf(TestException.class); + long endTime = System.nanoTime(); + + Duration duration = Duration.ofNanos(endTime - startTime); + assertThat(duration).isLessThan(Duration.ofSeconds(8)); + } + + @Test + public void retryOn() { + AtomicInteger counter = new AtomicInteger(); + + counter.set(0); + assertThatThrownBy(() -> bean.retryOn(new TestException(), counter)).isExactlyInstanceOf(TestException.class); + assertThat(counter).hasValue(1); + + counter.set(0); + assertThatThrownBy(() -> bean.retryOn(new TestConfigExceptionA(), counter)) + .isExactlyInstanceOf(TestConfigExceptionA.class); + assertThat(counter).hasValue(2); + + counter.set(0); + assertThatThrownBy(() -> bean.retryOn(new TestConfigExceptionB(), counter)) + .isExactlyInstanceOf(TestConfigExceptionB.class); + assertThat(counter).hasValue(2); + + counter.set(0); + assertThatThrownBy(() -> bean.retryOn(new TestConfigExceptionB1(), counter)) + .isExactlyInstanceOf(TestConfigExceptionB1.class); + assertThat(counter).hasValue(2); + } + + @Test + public void abortOn() { + AtomicInteger counter = new AtomicInteger(); + + counter.set(0); + assertThatThrownBy(() -> bean.abortOn(new TestException(), counter)).isExactlyInstanceOf(TestException.class); + assertThat(counter).hasValue(1); + + counter.set(0); + assertThatThrownBy(() -> bean.abortOn(new TestConfigExceptionA(), counter)) + .isExactlyInstanceOf(TestConfigExceptionA.class); + assertThat(counter).hasValue(1); + + counter.set(0); + assertThatThrownBy(() -> bean.abortOn(new TestConfigExceptionB(), counter)) + .isExactlyInstanceOf(TestConfigExceptionB.class); + assertThat(counter).hasValue(2); + + counter.set(0); + assertThatThrownBy(() -> bean.abortOn(new TestConfigExceptionB1(), counter)) + .isExactlyInstanceOf(TestConfigExceptionB1.class); + assertThat(counter).hasValue(1); + } + + @Test + public void jitter() { + assertThatThrownBy(() -> bean.jitter()).isExactlyInstanceOf(TestConfigExceptionA.class); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionA.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionA.java new file mode 100644 index 0000000000000..9fffc33f4981a --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionA.java @@ -0,0 +1,4 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +public class TestConfigExceptionA extends RuntimeException { +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionB.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionB.java new file mode 100644 index 0000000000000..76c68671a2046 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionB.java @@ -0,0 +1,4 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +public class TestConfigExceptionB extends RuntimeException { +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionB1.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionB1.java new file mode 100644 index 0000000000000..6637f93b1c495 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestConfigExceptionB1.java @@ -0,0 +1,4 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +public class TestConfigExceptionB1 extends TestConfigExceptionB { +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestException.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestException.java new file mode 100644 index 0000000000000..9ccf944cae795 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TestException.java @@ -0,0 +1,4 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +public class TestException extends RuntimeException { +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TimeoutConfigBean.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TimeoutConfigBean.java new file mode 100644 index 0000000000000..e214f556916e1 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TimeoutConfigBean.java @@ -0,0 +1,31 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import java.time.temporal.ChronoUnit; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Asynchronous; +import org.eclipse.microprofile.faulttolerance.Timeout; + +@ApplicationScoped +public class TimeoutConfigBean { + @Timeout(value = 1, unit = ChronoUnit.MILLIS) + public void value() throws InterruptedException { + Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + } + + @Timeout(value = 1000, unit = ChronoUnit.MICROS) + public void unit() throws InterruptedException { + Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + } + + @Timeout(value = 10, unit = ChronoUnit.MICROS) + @Asynchronous + public CompletionStage both() throws InterruptedException { + Thread.sleep(TimeUnit.MINUTES.toMillis(1)); + return CompletableFuture.completedFuture(null); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TimeoutConfigTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TimeoutConfigTest.java new file mode 100644 index 0000000000000..09a9fc4d1e631 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/config/TimeoutConfigTest.java @@ -0,0 +1,69 @@ +package io.quarkus.smallrye.faulttolerance.test.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import jakarta.inject.Inject; + +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class TimeoutConfigTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(TimeoutConfigBean.class)) + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.TimeoutConfigBean/value\".timeout.value", + "1000") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.TimeoutConfigBean/unit\".timeout.unit", + "millis") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.TimeoutConfigBean/both\".timeout.value", + "1000") + .overrideConfigKey( + "quarkus.fault-tolerance.\"io.quarkus.smallrye.faulttolerance.test.config.TimeoutConfigBean/both\".timeout.unit", + "millis"); + + @Inject + private TimeoutConfigBean bean; + + @Test + public void value() { + doTest(() -> bean.value()); + } + + @Test + public void unit() { + doTest(() -> bean.unit()); + } + + @Test + public void both() { + doTest(() -> { + try { + bean.both().toCompletableFuture().get(1, TimeUnit.MINUTES); + } catch (ExecutionException e) { + throw e.getCause(); + } + }); + } + + private void doTest(ThrowingCallable action) { + long start = System.nanoTime(); + assertThatThrownBy(action).isExactlyInstanceOf(TimeoutException.class); + long end = System.nanoTime(); + + long durationInMillis = Duration.ofNanos(end - start).toMillis(); + assertThat(durationInMillis).isGreaterThan(800); + assertThat(durationInMillis).isLessThan(2000); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/ExpectedOutcomeException.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/ExpectedOutcomeException.java new file mode 100644 index 0000000000000..773955962680f --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/ExpectedOutcomeException.java @@ -0,0 +1,10 @@ +package io.quarkus.smallrye.faulttolerance.test.fallback.causechain; + +public class ExpectedOutcomeException extends Exception { + public ExpectedOutcomeException() { + } + + public ExpectedOutcomeException(Throwable cause) { + super(cause); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithApplyOn.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithApplyOn.java new file mode 100644 index 0000000000000..813ac8ce88787 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithApplyOn.java @@ -0,0 +1,18 @@ +package io.quarkus.smallrye.faulttolerance.test.fallback.causechain; + +import java.io.IOException; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Fallback; + +@ApplicationScoped +public class FallbackWithApplyOn { + @Fallback(fallbackMethod = "fallback", applyOn = IOException.class) + public void hello(Exception e) throws Exception { + throw e; + } + + public void fallback(Exception ignored) { + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithBothSkipOnAndApplyOn.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithBothSkipOnAndApplyOn.java new file mode 100644 index 0000000000000..9cb2aea05b2d8 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithBothSkipOnAndApplyOn.java @@ -0,0 +1,18 @@ +package io.quarkus.smallrye.faulttolerance.test.fallback.causechain; + +import java.io.IOException; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Fallback; + +@ApplicationScoped +public class FallbackWithBothSkipOnAndApplyOn { + @Fallback(fallbackMethod = "fallback", skipOn = ExpectedOutcomeException.class, applyOn = IOException.class) + public void hello(Exception e) throws Exception { + throw e; + } + + public void fallback(Exception ignored) { + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithExceptionCauseChainTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithExceptionCauseChainTest.java new file mode 100644 index 0000000000000..40ce07c7d9d36 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithExceptionCauseChainTest.java @@ -0,0 +1,121 @@ +package io.quarkus.smallrye.faulttolerance.test.fallback.causechain; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.io.IOException; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; + +public class FallbackWithExceptionCauseChainTest { + @RegisterExtension + final static QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(ExpectedOutcomeException.class, FallbackWithApplyOn.class, + FallbackWithBothSkipOnAndApplyOn.class, FallbackWithSkipOn.class)); + + @Inject + FallbackWithBothSkipOnAndApplyOn fallbackWithBothSkipOnAndApplyOn; + + @Inject + FallbackWithSkipOn fallbackWithSkipOn; + + @Inject + FallbackWithApplyOn fallbackWithApplyOn; + + @Test + public void bothSkipOnAndApplyOn() { + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new RuntimeException())) + .isExactlyInstanceOf(RuntimeException.class); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new RuntimeException(new IOException()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new RuntimeException(new ExpectedOutcomeException()))) + .isExactlyInstanceOf(RuntimeException.class); + + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new Exception())) + .isExactlyInstanceOf(Exception.class); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new Exception(new IOException()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new Exception(new ExpectedOutcomeException()))) + .isExactlyInstanceOf(Exception.class); + + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new IOException())) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new IOException(new Exception()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new IOException(new ExpectedOutcomeException()))) + .doesNotThrowAnyException(); + + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new ExpectedOutcomeException())) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new ExpectedOutcomeException(new Exception()))) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + assertThatCode(() -> fallbackWithBothSkipOnAndApplyOn.hello(new ExpectedOutcomeException(new IOException()))) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + } + + @Test + public void skipOn() { + assertThatCode(() -> fallbackWithSkipOn.hello(new RuntimeException())) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithSkipOn.hello(new RuntimeException(new IOException()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithSkipOn.hello(new RuntimeException(new ExpectedOutcomeException()))) + .isExactlyInstanceOf(RuntimeException.class); + + assertThatCode(() -> fallbackWithSkipOn.hello(new Exception())) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithSkipOn.hello(new Exception(new IOException()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithSkipOn.hello(new Exception(new ExpectedOutcomeException()))) + .isExactlyInstanceOf(Exception.class); + + assertThatCode(() -> fallbackWithSkipOn.hello(new IOException())) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithSkipOn.hello(new IOException(new Exception()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithSkipOn.hello(new IOException(new ExpectedOutcomeException()))) + .isExactlyInstanceOf(IOException.class); + + assertThatCode(() -> fallbackWithSkipOn.hello(new ExpectedOutcomeException())) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + assertThatCode(() -> fallbackWithSkipOn.hello(new ExpectedOutcomeException(new Exception()))) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + assertThatCode(() -> fallbackWithSkipOn.hello(new ExpectedOutcomeException(new IOException()))) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + } + + @Test + public void applyOn() { + assertThatCode(() -> fallbackWithApplyOn.hello(new RuntimeException())) + .isExactlyInstanceOf(RuntimeException.class); + assertThatCode(() -> fallbackWithApplyOn.hello(new RuntimeException(new IOException()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithApplyOn.hello(new RuntimeException(new ExpectedOutcomeException()))) + .isExactlyInstanceOf(RuntimeException.class); + + assertThatCode(() -> fallbackWithApplyOn.hello(new Exception())) + .isExactlyInstanceOf(Exception.class); + assertThatCode(() -> fallbackWithApplyOn.hello(new Exception(new IOException()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithApplyOn.hello(new Exception(new ExpectedOutcomeException()))) + .isExactlyInstanceOf(Exception.class); + + assertThatCode(() -> fallbackWithApplyOn.hello(new IOException())) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithApplyOn.hello(new IOException(new Exception()))) + .doesNotThrowAnyException(); + assertThatCode(() -> fallbackWithApplyOn.hello(new IOException(new ExpectedOutcomeException()))) + .doesNotThrowAnyException(); + + assertThatCode(() -> fallbackWithApplyOn.hello(new ExpectedOutcomeException())) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + assertThatCode(() -> fallbackWithApplyOn.hello(new ExpectedOutcomeException(new Exception()))) + .isExactlyInstanceOf(ExpectedOutcomeException.class); + assertThatCode(() -> fallbackWithApplyOn.hello(new ExpectedOutcomeException(new IOException()))) + .doesNotThrowAnyException(); + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithSkipOn.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithSkipOn.java new file mode 100644 index 0000000000000..069ce3e7253b8 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/fallback/causechain/FallbackWithSkipOn.java @@ -0,0 +1,16 @@ +package io.quarkus.smallrye.faulttolerance.test.fallback.causechain; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.faulttolerance.Fallback; + +@ApplicationScoped +public class FallbackWithSkipOn { + @Fallback(fallbackMethod = "fallback", skipOn = ExpectedOutcomeException.class) + public void hello(Exception e) throws Exception { + throw e; + } + + public void fallback(Exception ignored) { + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/HelloService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/HelloService.java index 62d04c395ab0d..8b4654150a944 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/HelloService.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/HelloService.java @@ -8,7 +8,7 @@ import org.eclipse.microprofile.faulttolerance.CircuitBreaker; import io.smallrye.faulttolerance.api.CircuitBreakerName; -import io.smallrye.faulttolerance.api.FaultTolerance; +import io.smallrye.faulttolerance.api.TypedGuard; @ApplicationScoped public class HelloService { @@ -16,9 +16,10 @@ public class HelloService { static final int THRESHOLD = 5; static final int DELAY = 500; - private final Supplier anotherHello = FaultTolerance.createSupplier(this::anotherHelloImpl) + private final Supplier anotherHello = TypedGuard.create(String.class) .withCircuitBreaker().requestVolumeThreshold(THRESHOLD).delay(DELAY, ChronoUnit.MILLIS).name("another-hello").done() - .build(); + .build() + .adaptSupplier(this::anotherHelloImpl); @CircuitBreaker(requestVolumeThreshold = THRESHOLD, delay = DELAY) @CircuitBreakerName("hello") diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/ProgrammaticCircuitBreakerTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/ProgrammaticCircuitBreakerTest.java index 07484392b4adc..9564df40f32f2 100644 --- a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/ProgrammaticCircuitBreakerTest.java +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/programmatic/ProgrammaticCircuitBreakerTest.java @@ -18,12 +18,11 @@ import io.quarkus.test.QuarkusUnitTest; import io.smallrye.faulttolerance.api.CircuitBreakerMaintenance; import io.smallrye.faulttolerance.api.CircuitBreakerState; -import io.smallrye.faulttolerance.api.FaultTolerance; public class ProgrammaticCircuitBreakerTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() - .withApplicationRoot((jar) -> jar.addClasses(HelloService.class)); + .withApplicationRoot(jar -> jar.addClasses(HelloService.class)); @Inject HelloService helloService; @@ -33,14 +32,12 @@ public class ProgrammaticCircuitBreakerTest { @BeforeEach public void reset() { - FaultTolerance.circuitBreakerMaintenance().resetAll(); - - helloService.toString(); // force bean instantiation + CircuitBreakerMaintenance.get().resetAll(); } @Test public void test() { - CircuitBreakerMaintenance cbm = FaultTolerance.circuitBreakerMaintenance(); + CircuitBreakerMaintenance cbm = CircuitBreakerMaintenance.get(); assertThat(cb.currentState("hello")).isEqualTo(CircuitBreakerState.CLOSED); assertThat(cb.currentState("another-hello")).isEqualTo(CircuitBreakerState.CLOSED); diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/HelloService.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/HelloService.java new file mode 100644 index 0000000000000..e955934740942 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/HelloService.java @@ -0,0 +1,19 @@ +package io.quarkus.smallrye.faulttolerance.test.reuse; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.faulttolerance.api.ApplyGuard; + +@ApplicationScoped +public class HelloService { + static final String OK = "Hello"; + + @ApplyGuard("my-guard") + public String hello(Exception exception) throws Exception { + if (exception != null) { + throw exception; + } + + return OK; + } +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/MyGuard.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/MyGuard.java new file mode 100644 index 0000000000000..e7d992c7f60d2 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/MyGuard.java @@ -0,0 +1,21 @@ +package io.quarkus.smallrye.faulttolerance.test.reuse; + +import java.time.temporal.ChronoUnit; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.faulttolerance.api.Guard; + +@Singleton +public class MyGuard { + static final int THRESHOLD = 5; + static final int DELAY = 500; + + @Produces + @Identifier("my-guard") + public static final Guard GUARD = Guard.create() + .withCircuitBreaker().requestVolumeThreshold(THRESHOLD).delay(DELAY, ChronoUnit.MILLIS).name("hello").done() + .build(); +} diff --git a/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/ReuseCircuitBreakerTest.java b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/ReuseCircuitBreakerTest.java new file mode 100644 index 0000000000000..233c2c3703d7a --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/test/java/io/quarkus/smallrye/faulttolerance/test/reuse/ReuseCircuitBreakerTest.java @@ -0,0 +1,82 @@ +package io.quarkus.smallrye.faulttolerance.test.reuse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.awaitility.Awaitility.await; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.faulttolerance.exceptions.CircuitBreakerOpenException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.faulttolerance.api.CircuitBreakerMaintenance; +import io.smallrye.faulttolerance.api.CircuitBreakerState; + +public class ReuseCircuitBreakerTest { + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(jar -> jar.addClasses(HelloService.class, MyGuard.class)); + + @Inject + HelloService helloService; + + @Inject + CircuitBreakerMaintenance cb; + + @BeforeEach + public void reset() { + CircuitBreakerMaintenance.get().resetAll(); + } + + @Test + public void test() { + // force guard instantiation + assertThatCode(() -> { + helloService.hello(null); + }).doesNotThrowAnyException(); + + CircuitBreakerMaintenance cbm = CircuitBreakerMaintenance.get(); + + assertThat(cb.currentState("hello")).isEqualTo(CircuitBreakerState.CLOSED); + assertThat(cbm.currentState("hello")).isEqualTo(CircuitBreakerState.CLOSED); + + AtomicInteger helloStateChanges = new AtomicInteger(); + + cbm.onStateChange("hello", ignored -> { + helloStateChanges.incrementAndGet(); + }); + + for (int i = 0; i < MyGuard.THRESHOLD - 1; i++) { // `- 1` because of the initial invocation above + assertThatThrownBy(() -> { + helloService.hello(new IOException()); + }).isExactlyInstanceOf(IOException.class); + } + + assertThat(cb.currentState("hello")).isEqualTo(CircuitBreakerState.OPEN); + assertThat(cbm.currentState("hello")).isEqualTo(CircuitBreakerState.OPEN); + + // 1. closed -> open + assertThat(helloStateChanges).hasValue(1); + + await().atMost(MyGuard.DELAY * 2, TimeUnit.MILLISECONDS) + .ignoreException(CircuitBreakerOpenException.class) + .untilAsserted(() -> { + assertThat(helloService.hello(null)).isEqualTo(HelloService.OK); + }); + + assertThat(cb.currentState("hello")).isEqualTo(CircuitBreakerState.CLOSED); + assertThat(cbm.currentState("hello")).isEqualTo(CircuitBreakerState.CLOSED); + + // 2. open -> half-open + // 3. half-open -> closed + assertThat(helloStateChanges).hasValue(3); + } +} diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceBuildTimeConfig.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceBuildTimeConfig.java new file mode 100644 index 0000000000000..6dcdf2e6951fb --- /dev/null +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceBuildTimeConfig.java @@ -0,0 +1,72 @@ +package io.quarkus.smallrye.faulttolerance.runtime.config; + +import java.util.Map; +import java.util.Optional; + +import org.eclipse.microprofile.faulttolerance.Fallback; + +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithParentName; +import io.smallrye.faulttolerance.api.BeforeRetry; + +// this interface, as well as the nested interfaces, are never used; +// they only exist to signal to Quarkus that these config properties exist +@ConfigMapping(prefix = "quarkus.fault-tolerance") +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public interface SmallRyeFaultToleranceBuildTimeConfig { + /** + * Configuration of fault tolerance strategies; either global, per class, or per method. + * Keys are: + * + *
    + *
  • {@code global}: for global configuration
  • + *
  • {@code ""}: for per class configuration
  • + *
  • {@code "/"}: for per method configuration
  • + *
+ * + * Note that configuration follows the MicroProfile Fault Tolerance specification. + * That is, if an annotation is present on a method, the configuration must be per method; + * if an annotation is present on a class, the configuration must be per class. + * Global configuration is a fallback for both per method and per class configuration, + * but per class configuration is not a fallback for per method configuration. + */ + @WithParentName + @ConfigDocMapKey("") + Map strategies(); + + interface StrategiesConfig { + /** + * Configuration of the {@code @BeforeRetry} fault tolerance strategy. + */ + Optional beforeRetry(); + + /** + * Configuration of the {@code @Fallback} fault tolerance strategy. + */ + Optional fallback(); + + interface BeforeRetryConfig { + /** + * The name of the method to call before retrying. The method belongs to the same class + * as the guarded method. The method must have no parameters and return {@code void}. + * + * @see BeforeRetry#methodName() + */ + Optional methodName(); + } + + interface FallbackConfig { + /** + * The name of the method to call on fallback. The method belongs to the same class + * as the guarded method. The method must have a signature matching the signature + * of the guarded method. + * + * @see Fallback#fallbackMethod() + */ + Optional fallbackMethod(); + } + } +} diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceConfig.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceConfig.java new file mode 100644 index 0000000000000..d9c7d08948198 --- /dev/null +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceConfig.java @@ -0,0 +1,577 @@ +package io.quarkus.smallrye.faulttolerance.runtime.config; + +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; +import java.util.function.Predicate; + +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 io.quarkus.runtime.annotations.ConfigDocDefault; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithName; +import io.smallrye.config.WithParentName; +import io.smallrye.faulttolerance.api.ApplyGuard; +import io.smallrye.faulttolerance.api.BeforeRetry; +import io.smallrye.faulttolerance.api.BeforeRetryHandler; +import io.smallrye.faulttolerance.api.CustomBackoff; +import io.smallrye.faulttolerance.api.CustomBackoffStrategy; +import io.smallrye.faulttolerance.api.ExponentialBackoff; +import io.smallrye.faulttolerance.api.FibonacciBackoff; +import io.smallrye.faulttolerance.api.RateLimit; +import io.smallrye.faulttolerance.api.RateLimitType; +import io.smallrye.faulttolerance.api.RetryWhen; + +// this interface, as well as the nested interfaces, are never used; +// they only exist to signal to Quarkus that these config properties exist +@ConfigMapping(prefix = "quarkus.fault-tolerance") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface SmallRyeFaultToleranceConfig { + /** + * Whether fault tolerance strategies are enabled. Note that {@code @Fallback} + * is always enabled, this applies to all other strategies. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * Whether fault tolerance metrics are enabled. + */ + @WithName("metrics.enabled") + @ConfigDocDefault("true") + Optional metricsEnabled(); + + /** + * Whether SmallRye Fault Tolerance should be compatible with the MicroProfile + * Fault Tolerance specification. + */ + @ConfigDocDefault("false") + Optional mpCompatibility(); + + /** + * Configuration of fault tolerance strategies; either global, per class, or per method. + * Keys are: + * + *
    + *
  • {@code global}: for global configuration
  • + *
  • {@code ""}: for per class configuration
  • + *
  • {@code "/"}: for per method configuration
  • + *
+ * + * Note that configuration follows the MicroProfile Fault Tolerance specification. + * That is, if an annotation is present on a method, the configuration must be per method; + * if an annotation is present on a class, the configuration must be per class. + * Global configuration is a fallback for both per method and per class configuration, + * but per class configuration is not a fallback for per method configuration. + */ + @WithParentName + @ConfigDocMapKey("") + Map strategies(); + + interface StrategiesConfig { + /** + * Configuration of the {@code @ApplyGuard} fault tolerance strategy. + */ + Optional applyGuard(); + + /** + * Configuration of the {@code @Asynchronous} fault tolerance strategy. + */ + Optional asynchronous(); + + /** + * Configuration of the {@code @AsynchronousNonBlocking} fault tolerance strategy. + */ + Optional asynchronousNonBlocking(); + + /** + * Configuration of the {@code @BeforeRetry} fault tolerance strategy. + */ + Optional beforeRetry(); + + /** + * Configuration of the {@code @Bulkhead} fault tolerance strategy. + */ + Optional bulkhead(); + + /** + * Configuration of the {@code @CircuitBreaker} fault tolerance strategy. + */ + Optional circuitBreaker(); + + /** + * Configuration of the {@code @CustomBackoff} fault tolerance strategy. + */ + Optional customBackoff(); + + /** + * Configuration of the {@code @ExponentialBackoff} fault tolerance strategy. + */ + Optional exponentialBackoff(); + + /** + * Configuration of the {@code @Fallback} fault tolerance strategy. + */ + Optional fallback(); + + /** + * Configuration of the {@code @FibonacciBackoff} fault tolerance strategy. + */ + Optional fibonacciBackoff(); + + /** + * Configuration of the {@code @RateLimit} fault tolerance strategy. + */ + Optional rateLimit(); + + /** + * Configuration of the {@code @Retry} fault tolerance strategy. + */ + Optional retry(); + + /** + * Configuration of the {@code @RetryWhen} fault tolerance strategy. + */ + Optional retryWhen(); + + /** + * Configuration of the {@code @Timeout} fault tolerance strategy. + */ + Optional timeout(); + + interface ApplyGuardConfig { + /** + * Whether the {@code @ApplyGuard} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The {@link io.smallrye.common.annotation.Identifier @Identifier} + * of the {@link io.smallrye.faulttolerance.api.Guard Guard} + * or {@link io.smallrye.faulttolerance.api.TypedGuard TypedGuard} + * to use on the annotated method. + * + * @see ApplyGuard#value() + */ + Optional value(); + } + + interface AsynchronousConfig { + /** + * Whether the {@code @Asynchronous} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + } + + interface AsynchronousNonBlockingConfig { + /** + * Whether the {@code @AsynchronousNonBlocking} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + } + + interface BeforeRetryConfig { + /** + * Whether the {@code @BeforeRetry} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The class of the {@link BeforeRetryHandler} to call before retrying. + * + * @see BeforeRetry#value() + */ + Optional> value(); + } + + interface BulkheadConfig { + /** + * Whether the {@code @Bulkhead} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The maximum number of concurrent invocations. + * + * @see Bulkhead#value() + */ + @ConfigDocDefault("10") + OptionalInt value(); + + /** + * The maximum number of queued asynchronous invocations. Asynchronous invocations are queued + * when the number of concurrent invocations in progress has already reached the maximum. + * Synchronous invocations are not queued at all and are rejected immediately. + * + * @see Bulkhead#waitingTaskQueue() + */ + @ConfigDocDefault("10") + OptionalInt waitingTaskQueue(); + } + + interface CircuitBreakerConfig { + /** + * Whether the {@code @CircuitBreaker} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The delay after which an open circuit breaker will move to half-open. + * + * @see CircuitBreaker#delay() + */ + @ConfigDocDefault("5 seconds") + OptionalLong delay(); + + /** + * The unit for {@link #delay()}. + * + * @see CircuitBreaker#delayUnit() + */ + Optional delayUnit(); + + /** + * The exception types that are considered failures. + * + * @see CircuitBreaker#failOn() + */ + @ConfigDocDefault("Throwable (all exceptions)") + Optional[]> failOn(); + + /** + * The ratio of failures within the rolling window that will move a closed circuit breaker to open. + * + * @see CircuitBreaker#failureRatio() + */ + @ConfigDocDefault("0.5") + OptionalDouble failureRatio(); + + /** + * The size of the circuit breaker rolling window. + * + * @see CircuitBreaker#requestVolumeThreshold() + */ + @ConfigDocDefault("20") + OptionalInt requestVolumeThreshold(); + + /** + * The exception types that are not considered failures. Takes priority over {@link #failOn()}. + * + * @see CircuitBreaker#skipOn() + */ + @ConfigDocDefault("") + Optional[]> skipOn(); + + /** + * The number of successful executions that move a half-open circuit breaker to closed. + * + * @see CircuitBreaker#successThreshold() + */ + @ConfigDocDefault("1") + OptionalInt successThreshold(); + } + + interface CustomBackoffConfig { + /** + * Whether the {@code @CustomBackoff} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The class of the {@link CustomBackoffStrategy} that will be used to compute retry delays. + * + * @see CustomBackoff#value() + */ + Optional> value(); + } + + interface ExponentialBackoffConfig { + /** + * Whether the {@code @ExponentialBackoff} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The multiplicative factor used when determining a delay between two retries. A delay is computed + * as {@code factor * previousDelay}, resulting in an exponential growth. + * + * @see ExponentialBackoff#factor() + */ + @ConfigDocDefault("2") + OptionalInt factor(); + + /** + * The maximum delay between retries. + * + * @see ExponentialBackoff#maxDelay() + */ + @ConfigDocDefault("1 minute") + OptionalLong maxDelay(); + + /** + * The unit for {@link #maxDelay()}. + * + * @see ExponentialBackoff#maxDelayUnit() + */ + Optional maxDelayUnit(); + } + + interface FallbackConfig { + /** + * Whether the {@code @Fallback} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The exception types that are considered failures and hence should trigger fallback. + * + * @see Fallback#applyOn() + */ + @ConfigDocDefault("Throwable (all exceptions)") + Optional[]> applyOn(); + + /** + * The exception types that are not considered failures and hence should not trigger fallback. + * Takes priority over {@link #applyOn()}}. + * + * @see Fallback#skipOn() + */ + @ConfigDocDefault("") + Optional[]> skipOn(); + + /** + * The class of the {@link FallbackHandler} to call on fallback. + * + * @see Fallback#value() + */ + Optional>> value(); + } + + interface FibonacciBackoffConfig { + /** + * Whether the {@code @FibonacciBackoff} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The maximum delay between retries. + * + * @see FibonacciBackoff#maxDelay() + */ + @ConfigDocDefault("1 minute") + OptionalLong maxDelay(); + + /** + * The unit for {@link #maxDelay()}. + * + * @see FibonacciBackoff#maxDelayUnit() + */ + Optional maxDelayUnit(); + } + + interface RateLimitConfig { + /** + * Whether the {@code @RateLimit} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * Minimum time between two consecutive invocations. If the time between two consecutive + * invocations is shorter, the second invocation is rejected. + * + * @see RateLimit#minSpacing() + */ + @ConfigDocDefault("0") + OptionalLong minSpacing(); + + /** + * The unit for {@link #minSpacing()}. + * + * @see RateLimit#minSpacingUnit() + */ + Optional minSpacingUnit(); + + /** + * The type of type windows used for rate limiting. + * + * @see RateLimit#type() + */ + @ConfigDocDefault("fixed") + Optional type(); + + /** + * The maximum number of invocations in a time window. + * + * @see RateLimit#value() + */ + @ConfigDocDefault("100") + OptionalInt value(); + + /** + * The time window length. + * + * @see RateLimit#window() + */ + @ConfigDocDefault("1 second") + OptionalLong window(); + + /** + * The unit for {@link #window()}. + * + * @see RateLimit#windowUnit() + */ + Optional windowUnit(); + } + + interface RetryConfig { + /** + * Whether the {@code @Retry} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The exception types that are not considered failures and hence should not be retried. + * Takes priority over {@link #retryOn()}. + * + * @see Retry#abortOn() + */ + @ConfigDocDefault("") + Optional[]> abortOn(); + + /** + * The delay between retry attempts. + * + * @see Retry#delay() + */ + @ConfigDocDefault("0") + OptionalLong delay(); + + /** + * The unit for {@link #delay()}. + * + * @see Retry#delayUnit() + */ + Optional delayUnit(); + + /** + * The maximum jitter to apply for the delay between retry attempts. + * The actual delay will be in the interval {@code [delay - jitter, delay + jitter]}, + * but will not be negative. + * + * @see Retry#jitter() + */ + @ConfigDocDefault("200 millis") + OptionalLong jitter(); + + /** + * The unit for {@link #jitter()}. + * + * @see Retry#jitterDelayUnit() + */ + Optional jitterUnit(); // `Retry.jitterDelayUnit()` + + /** + * The maximum duration for which to retry. + * + * @see Retry#maxDuration() + */ + @ConfigDocDefault("3 minutes") + OptionalLong maxDuration(); + + /** + * The unit for {@link #maxDuration()}. + * + * @see Retry#durationUnit() + */ + Optional maxDurationUnit(); // `Retry.durationUnit()` + + /** + * The maximum number of retry attempts. + * + * @see Retry#maxRetries() + */ + @ConfigDocDefault("3") + OptionalInt maxRetries(); + + /** + * The exception types that are considered failures and hence should be retried. + * + * @see Retry#retryOn() + */ + @ConfigDocDefault("Exception (all exceptions)") + Optional[]> retryOn(); + } + + interface RetryWhenConfig { + /** + * Whether the {@code @RetryWhen} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * Class of the predicate that will be used to determine whether the invocation should be retried + * if the guarded method has thrown an exception. + * + * @see RetryWhen#exception() + */ + @ConfigDocDefault("AlwaysOnException") + Optional>> exception(); + + /** + * Class of the predicate that will be used to determine whether the invocation should be retried + * if the guarded method has returned a result. + * + * @see RetryWhen#result() + */ + @ConfigDocDefault("NeverOnResult") + Optional>> result(); + } + + interface TimeoutConfig { + /** + * Whether the {@code @Timeout} strategy is enabled. + */ + @ConfigDocDefault("true") + Optional enabled(); + + /** + * The unit for {@link #value()}. + * + * @see Timeout#unit() + */ + Optional unit(); + + /** + * The timeout to enforce. + * + * @see Timeout#value() + */ + @ConfigDocDefault("1 second") + OptionalLong value(); + } + } +} diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceConfigRelocate.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceConfigRelocate.java new file mode 100644 index 0000000000000..c8af14768f2cb --- /dev/null +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/config/SmallRyeFaultToleranceConfigRelocate.java @@ -0,0 +1,18 @@ +package io.quarkus.smallrye.faulttolerance.runtime.config; + +import java.util.function.Function; + +import io.smallrye.config.RelocateConfigSourceInterceptor; + +public class SmallRyeFaultToleranceConfigRelocate extends RelocateConfigSourceInterceptor { + private static final Function RELOCATION = name -> { + if (name.startsWith("smallrye.faulttolerance.")) { + return name.replaceFirst("smallrye\\.faulttolerance\\.", "quarkus.fault-tolerance."); + } + return name; + }; + + public SmallRyeFaultToleranceConfigRelocate() { + super(RELOCATION); + } +} diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java index 0068e9b1a2d49..0159d4dc3f7cc 100644 --- a/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/java/io/quarkus/smallrye/faulttolerance/runtime/devui/FaultToleranceJsonRpcService.java @@ -16,6 +16,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; @@ -53,6 +54,10 @@ private JsonObject convert(FaultToleranceOperation operation) { result.put(ApplyFaultTolerance.class.getSimpleName(), new JsonObject() .put("value", operation.getApplyFaultTolerance().value())); } + if (operation.hasApplyGuard()) { + result.put(ApplyGuard.class.getSimpleName(), new JsonObject() + .put("value", operation.getApplyGuard().value())); + } if (operation.hasAsynchronous()) { result.put(Asynchronous.class.getSimpleName(), new JsonObject()); diff --git a/extensions/smallrye-fault-tolerance/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor b/extensions/smallrye-fault-tolerance/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor new file mode 100644 index 0000000000000..7f622f91114cd --- /dev/null +++ b/extensions/smallrye-fault-tolerance/runtime/src/main/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptor @@ -0,0 +1 @@ +io.quarkus.smallrye.faulttolerance.runtime.config.SmallRyeFaultToleranceConfigRelocate diff --git a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/PreconfiguredFaultTolerance.java b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/PreconfiguredFaultTolerance.java index 720fc6abb0450..77f06fb2f6087 100644 --- a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/PreconfiguredFaultTolerance.java +++ b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/PreconfiguredFaultTolerance.java @@ -4,13 +4,13 @@ import jakarta.enterprise.inject.Produces; import io.smallrye.common.annotation.Identifier; -import io.smallrye.faulttolerance.api.FaultTolerance; +import io.smallrye.faulttolerance.api.Guard; @ApplicationScoped public class PreconfiguredFaultTolerance { @Produces @Identifier("my-fault-tolerance") - public static final FaultTolerance FT = FaultTolerance. create() + public static final Guard GUARD = Guard.create() .withRetry().maxRetries(10).done() .build(); } diff --git a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java index 72e0facaa7e72..cc45c4143d7b5 100644 --- a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java +++ b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java @@ -8,7 +8,7 @@ import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; -import io.smallrye.faulttolerance.api.ApplyFaultTolerance; +import io.smallrye.faulttolerance.api.ApplyGuard; @ApplicationScoped public class Service { @@ -22,7 +22,7 @@ void init() { name = "Lucie"; } - @ApplyFaultTolerance("my-fault-tolerance") + @ApplyGuard("my-fault-tolerance") public String getName(AtomicInteger counter) { if (counter.incrementAndGet() >= THRESHOLD) { return name;