diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTryTaskBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTryTaskBuilder.java index 76eb598ac..1dc14b59d 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTryTaskBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/BaseTryTaskBuilder.java @@ -113,6 +113,11 @@ public TryTaskCatchBuilder retry(Consumer consumer) { return this; } + public TryTaskCatchBuilder retry(String reference) { + this.tryTaskCatch.setRetry(new Retry().withRetryPolicyReference(reference)); + return this; + } + public TryTaskCatchBuilder errorsWith(Consumer consumer) { final CatchErrorsBuilder catchErrorsBuilder = new CatchErrorsBuilder(); consumer.accept(catchErrorsBuilder); diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/UseBuilder.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/UseBuilder.java index 5398be311..c6c1d9970 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/UseBuilder.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/UseBuilder.java @@ -19,13 +19,18 @@ import io.serverlessworkflow.api.types.ErrorDetails; import io.serverlessworkflow.api.types.ErrorTitle; import io.serverlessworkflow.api.types.ErrorType; +import io.serverlessworkflow.api.types.RetryPolicy; import io.serverlessworkflow.api.types.UriTemplate; import io.serverlessworkflow.api.types.Use; import io.serverlessworkflow.api.types.UseAuthentications; import io.serverlessworkflow.api.types.UseErrors; +import io.serverlessworkflow.api.types.UseRetries; +import io.serverlessworkflow.fluent.spec.BaseTryTaskBuilder.RetryPolicyBuilder; import java.net.URI; import java.net.URISyntaxException; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; public class UseBuilder { @@ -66,6 +71,17 @@ public UseBuilder errors(Consumer errorsConsumer) { return this; } + public UseBuilder retries(Consumer retriesConsumer) { + final UseRetriesBuilder retriesBuilder = new UseRetriesBuilder(); + retriesConsumer.accept(retriesBuilder); + final UseRetries built = retriesBuilder.build(); + if (this.use.getRetries() == null) { + this.use.setRetries(new UseRetries()); + } + this.use.getRetries().getAdditionalProperties().putAll(built.getAdditionalProperties()); + return this; + } + public Use build() { return use; } @@ -132,4 +148,23 @@ public Error build() { return error; } } + + public static final class UseRetriesBuilder { + private final Map retries = new LinkedHashMap<>(); + + UseRetriesBuilder() {} + + public UseRetriesBuilder retry(String name, Consumer configurer) { + final RetryPolicyBuilder policyBuilder = new RetryPolicyBuilder(); + configurer.accept(policyBuilder); + this.retries.put(name, policyBuilder.build()); + return this; + } + + public UseRetries build() { + final UseRetries useRetries = new UseRetries(); + useRetries.getAdditionalProperties().putAll(this.retries); + return useRetries; + } + } } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java index bd58b3e58..5fe3904c4 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java @@ -122,6 +122,11 @@ default SELF query(String name, String value) { return self(); } + default SELF redirect(boolean redirect) { + steps().add(c -> c.redirect(redirect)); + return self(); + } + default void accept(CallHttpTaskFluent b) { for (var s : steps()) { s.accept(b); diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java index 5649c6968..0c80159ed 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/RetrySpec.java @@ -15,6 +15,7 @@ */ package io.serverlessworkflow.fluent.spec.dsl; +import io.serverlessworkflow.fluent.spec.DurationInlineBuilder; import io.serverlessworkflow.fluent.spec.TryTaskBuilder; import io.serverlessworkflow.fluent.spec.configurers.RetryConfigurer; import java.util.LinkedList; @@ -51,11 +52,49 @@ public RetrySpec limit(Consumer retry) { return this; } + public RetrySpec delay(String expression) { + steps.add(r -> r.delay(expression)); + return this; + } + + /** + * Configures an inline delay using a duration builder. + * + * @see #delay(String) + */ + public RetrySpec delay(Consumer duration) { + steps.add(r -> r.delay(duration)); + return this; + } + public RetrySpec backoff(Consumer backoff) { steps.add(r -> r.backoff(backoff)); return this; } + /** + * Configures exponential backoff with identifier "e" and a default factor of "1.5". This is a + * convenience shortcut; for full control use {@link #backoff(Consumer)}. + * + * @return this spec + */ + public RetrySpec backoffExponential() { + steps.add(r -> r.backoff(b -> b.exponential("e", "1.5"))); + return this; + } + + /** + * Configures constant backoff with identifier "c" and a default delay of "10" (units unspecified + * by the spec; typically milliseconds). This is a convenience shortcut; for full control use + * {@link #backoff(Consumer)}. + * + * @return this spec + */ + public RetrySpec backoffConstant() { + steps.add(r -> r.backoff(b -> b.constant("c", "10"))); + return this; + } + public RetrySpec jitter(Consumer jitter) { steps.add(r -> r.jitter(jitter)); return this; diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java index 27733fe96..db9858949 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchSpec.java @@ -64,6 +64,16 @@ public TryCatchSpec errors(Consumer errors) { return this; } + public TryCatchSpec as(String errorVarName) { + steps.add(t -> t.as(errorVarName)); + return this; + } + + public TryCatchSpec retry(String reference) { + steps.add(t -> t.retry(reference)); + return this; + } + public RetrySpec retry() { return retry; } diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/UseSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/UseSpec.java index 1e7286fad..547f189f9 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/UseSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/UseSpec.java @@ -46,6 +46,11 @@ public UseSpec errors(Consumer errorsConsumer) { return this; } + public UseSpec retries(Consumer retriesConsumer) { + steps.add(u -> u.retries(retriesConsumer)); + return this; + } + @Override public void accept(UseBuilder useBuilder) { steps.forEach(step -> step.accept(useBuilder)); diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java index e62b28898..5ef93894d 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/dsl/TryCatchDslTest.java @@ -17,9 +17,12 @@ import static io.serverlessworkflow.fluent.spec.dsl.DSL.call; import static io.serverlessworkflow.fluent.spec.dsl.DSL.emit; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.error; import static io.serverlessworkflow.fluent.spec.dsl.DSL.http; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.raise; import static io.serverlessworkflow.fluent.spec.dsl.DSL.set; import static io.serverlessworkflow.fluent.spec.dsl.DSL.tryCatch; +import static io.serverlessworkflow.fluent.spec.dsl.DSL.use; import static org.assertj.core.api.Assertions.assertThat; import io.serverlessworkflow.api.types.Workflow; @@ -205,4 +208,418 @@ void when_try_with_catch_and_simple_retry_limit_only() { var ev = catchDo.get(0).getTask().getEmitTask().getEmit().getEvent().getWith(); assertThat(ev.getType()).isEqualTo("org.acme.retrying"); } + + @Test + void when_try_catch_with_do_task() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-with-do", "test", "0.1.0") + .tasks( + tryCatch( + "attemptTask", + tryCatch() + .tasks( + raise( + "failingTask", + error(URI.create("https://example.com/errors/runtime"), 500))) + .catches() + .errors(URI.create("https://example.com/errors/runtime"), 500) + .tasks( + set( + "executeAfterFailingTask", + s -> s.put("setAfterFailingTask", "No Problem"))) + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getErrors().getWith().getType()).isEqualTo("https://example.com/errors/runtime"); + assertThat(cat.getErrors().getWith().getStatus()).isEqualTo(500); + var catchDo = cat.getDo(); + assertThat(catchDo).hasSize(1); + var setTask = catchDo.get(0).getTask().getSetTask(); + assertThat(setTask).isNotNull(); + assertThat( + setTask + .getSet() + .getSetTaskConfiguration() + .getAdditionalProperties() + .get("setAfterFailingTask")) + .isEqualTo("No Problem"); + } + + @Test + void when_try_catch_match_status() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-match-status", "test", "0.1.0") + .tasks( + tryCatch( + "attemptTask", + tryCatch() + .tasks( + raise( + "failingTask", + error(URI.create("https://example.com/errors/transient"), 503))) + .catches() + .errors(URI.create("https://example.com/errors/transient"), 503) + .tasks(set("handleError", s -> s.put("recovered", true))) + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getErrors().getWith().getStatus()).isEqualTo(503); + var catchDo = cat.getDo(); + assertThat(catchDo).hasSize(1); + } + + @Test + void when_try_catch_not_match_status() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-not-match-status", "test", "0.1.0") + .tasks( + tryCatch( + "attemptTask", + tryCatch() + .tasks( + raise( + "failingTask", + error(URI.create("https://example.com/errors/transient"), 503))) + .catches() + .errors(URI.create("https://example.com/errors/transient"), 403) + .tasks(set("handleError", s -> s.put("recovered", true))) + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getErrors().getWith().getStatus()).isEqualTo(403); + } + + @Test + void when_try_catch_match_details() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-match-details", "test", "0.1.0") + .tasks( + tryCatch( + "attemptTask", + tryCatch() + .tasks( + raise( + "failingTask", + error(URI.create("https://example.com/errors/transient"), 503) + .detail("Enforcement Failure - invalid email"))) + .catches() + .errors( + e -> + e.type("https://example.com/errors/transient") + .status(503) + .details("Enforcement Failure - invalid email")) + .tasks(set("handleError", s -> s.put("recovered", true))) + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getErrors().getWith().getDetails()) + .isEqualTo("Enforcement Failure - invalid email"); + } + + @Test + void when_try_catch_match_when() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-match-when", "test", "0.1.0") + .tasks( + tryCatch( + "attemptTask", + tryCatch() + .tasks( + raise( + "failingTask", + error(URI.create("https://example.com/errors/transient"), 503))) + .catches() + .when("${ .status == 503 }") + .tasks(set("handleError", s -> s.put("recovered", true))) + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getWhen()).isEqualTo("${ .status == 503 }"); + } + + @Test + void when_try_catch_error_variable() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-error-variable", "test", "0.1.0") + .tasks( + tryCatch( + "attemptTask", + tryCatch() + .tasks( + raise( + "failingTask", + error(URI.create("https://example.com/errors/transient"), 503) + .detail("Javierito was here!"))) + .catches() + .as("caughtError") + .tasks( + set( + "handleError", + s -> s.put("errorMessage", "${$caughtError.details}"))) + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getAs()).isEqualTo("caughtError"); + var catchDo = cat.getDo(); + assertThat(catchDo).hasSize(1); + var setTask = catchDo.get(0).getTask().getSetTask(); + assertThat( + setTask + .getSet() + .getSetTaskConfiguration() + .getAdditionalProperties() + .get("errorMessage")) + .isEqualTo("${$caughtError.details}"); + } + + @Test + void when_try_catch_inline_retry() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-retry-inline", "test", "0.1.0") + .tasks( + tryCatch( + "tryGetPet", + tryCatch() + .tasks( + call( + "getPet", + http().GET().endpoint("http://localhost:9797").redirect(true))) + .catches() + .errors(Errors.COMMUNICATION, 404) + .retry() + .delay("${\"PT\\(.delay)S\"}") + .backoff(b -> b.exponential("e", "1.5")) + .limit(l -> l.attempt(a -> a.count(5))) + .done() + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + var retryDef = cat.getRetry().getRetryPolicyDefinition(); + assertThat(retryDef).isNotNull(); + assertThat(retryDef.getDelay().getDurationExpression()).isEqualTo("${\"PT\\(.delay)S\"}"); + assertThat(retryDef.getLimit().getAttempt().getCount()).isEqualTo(5); + } + + @Test + void when_try_catch_reusable_retry() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-retry-reusable", "test", "0.1.0") + .use( + use() + .retries( + r -> + r.retry( + "default", + policy -> + policy + .delay("PT0.01S") + .backoff(b -> b.constant("c", "10")) + .limit(l -> l.attempt(a -> a.count(5)))))) + .tasks( + tryCatch( + "tryGetPet", + tryCatch() + .tasks( + call( + "getPet", + http().GET().endpoint("http://localhost:9797").redirect(true))) + .catches() + .errors(Errors.COMMUNICATION, 404) + .retry("default") + .done())) + .build(); + + var useBlock = wf.getUse(); + assertThat(useBlock).isNotNull(); + assertThat(useBlock.getRetries()).isNotNull(); + assertThat(useBlock.getRetries().getAdditionalProperties()).containsKey("default"); + assertThat( + useBlock + .getRetries() + .getAdditionalProperties() + .get("default") + .getLimit() + .getAttempt() + .getCount()) + .isEqualTo(5); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + assertThat(cat.getRetry().getRetryPolicyReference()).isEqualTo("default"); + } + + @Test + void when_nested_try_catch() { + Workflow wf = + WorkflowBuilder.workflow("nested-try-catch-retry-inline", "test", "0.1.0") + .tasks( + tryCatch( + "tryServerError", + tryCatch() + .tasks( + tryCatch( + "tryCommunication", + tryCatch() + .tasks( + call( + "getPet", + http() + .GET() + .endpoint("http://localhost:9797") + .redirect(true))) + .catches() + .errors(Errors.COMMUNICATION, 404) + .retry() + .delay("${\"PT\\(.delay)S\"}") + .backoff(b -> b.exponential("e", "1.5")) + .limit(l -> l.attempt(a -> a.count(5))) + .done() + .done())) + .catches() + .errors(Errors.COMMUNICATION, 404) + .retry() + .delay("${\"PT\\(.delay)S\"}") + .backoff(b -> b.exponential("e", "1.5")) + .limit(l -> l.attempt(a -> a.count(2))) + .done() + .done())) + .build(); + + var outerTry = wf.getDo().get(0).getTask().getTryTask(); + assertThat(outerTry).isNotNull(); + var outerCatch = outerTry.getCatch(); + assertThat(outerCatch).isNotNull(); + assertThat(outerCatch.getErrors().getWith().getStatus()).isEqualTo(404); + + var innerTry = outerTry.getTry().get(0).getTask().getTryTask(); + assertThat(innerTry).isNotNull(); + var innerCatch = innerTry.getCatch(); + assertThat(innerCatch).isNotNull(); + assertThat(innerCatch.getErrors().getWith().getStatus()).isEqualTo(404); + } + + @Test + void when_try_catch_inline_retry_with_duration_builder() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-retry-duration-builder", "test", "0.1.0") + .tasks( + tryCatch( + "tryGetPet", + tryCatch() + .tasks( + call( + "getPet", + http().GET().endpoint("http://localhost:9797").redirect(true))) + .catches() + .errors(Errors.COMMUNICATION, 404) + .retry() + .delay(d -> d.milliseconds(100)) + .limit("PT1S") + .done() + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + var retryDef = cat.getRetry().getRetryPolicyDefinition(); + assertThat(retryDef).isNotNull(); + assertThat(retryDef.getDelay().getDurationInline().getMilliseconds()).isEqualTo(100); + assertThat(retryDef.getLimit().getDuration().getDurationLiteral()).isEqualTo("PT1S"); + } + + @Test + void when_try_catch_backoff_exponential() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-backoff-exponential", "test", "0.1.0") + .tasks( + tryCatch( + "tryTask", + tryCatch() + .tasks(call(http().GET().endpoint("http://localhost:9797"))) + .catches() + .errors(Errors.COMMUNICATION, 404) + .retry() + .backoffExponential() + .done() + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + var retryDef = cat.getRetry().getRetryPolicyDefinition(); + assertThat(retryDef).isNotNull(); + var backoff = retryDef.getBackoff(); + assertThat(backoff).isNotNull(); + var expBackoff = backoff.getExponentialBackOff(); + assertThat(expBackoff).isNotNull(); + var exp = expBackoff.getExponential(); + assertThat(exp).isNotNull(); + assertThat(exp.getAdditionalProperties()).containsEntry("e", "1.5"); + } + + @Test + void when_try_catch_backoff_constant() { + Workflow wf = + WorkflowBuilder.workflow("try-catch-backoff-constant", "test", "0.1.0") + .tasks( + tryCatch( + "tryTask", + tryCatch() + .tasks(call(http().GET().endpoint("http://localhost:9797"))) + .catches() + .errors(Errors.COMMUNICATION, 404) + .retry() + .backoffConstant() + .done() + .done())) + .build(); + + var tryTask = wf.getDo().get(0).getTask().getTryTask(); + assertThat(tryTask).isNotNull(); + var cat = tryTask.getCatch(); + assertThat(cat).isNotNull(); + var retryDef = cat.getRetry().getRetryPolicyDefinition(); + assertThat(retryDef).isNotNull(); + var backoff = retryDef.getBackoff(); + assertThat(backoff).isNotNull(); + var constBackoff = backoff.getConstantBackoff(); + assertThat(constBackoff).isNotNull(); + var constant = constBackoff.getConstant(); + assertThat(constant).isNotNull(); + assertThat(constant.getAdditionalProperties()).containsEntry("c", "10"); + } }