From de243ffe8d770fcb98abd6ae48c02bd39a41eff7 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Mon, 5 May 2025 10:40:33 -0500 Subject: [PATCH 01/22] add basic compensation example for wf Signed-off-by: Cassandra Coyle --- .../compensation/BookCarActivity.java | 42 +++++++++++ .../compensation/BookFlightActivity.java | 41 ++++++++++ .../compensation/BookHotelActivity.java | 40 ++++++++++ .../compensation/BookTripClient.java | 35 +++++++++ .../compensation/BookTripWorker.java | 38 ++++++++++ .../compensation/BookTripWorkflow.java | 75 +++++++++++++++++++ .../compensation/CancelCarActivity.java | 41 ++++++++++ .../compensation/CancelFlightActivity.java | 41 ++++++++++ .../compensation/CancelHotelActivity.java | 41 ++++++++++ .../examples/workflows/compensation/README.md | 47 ++++++++++++ 10 files changed, 441 insertions(+) create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/README.md diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java new file mode 100644 index 0000000000..e96f854a70 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class BookCarActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookCarActivity.class); + + @Override + public String run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + // Simulate work + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + logger.info("Forcing Failure to trigger compensation for activity: " + ctx.getName()); + + // force the compensation + throw new RuntimeException("Failed to book car"); + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java new file mode 100644 index 0000000000..0f76a498c0 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class BookFlightActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookFlightActivity.class); + + @Override + public String run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + // Simulate work + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + String result = "Flight booked successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java new file mode 100644 index 0000000000..307c9c14cb --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BookHotelActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookHotelActivity.class); + + @Override + public String run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + logger.info("Simulating hotel booking process..."); + + // Simulate some work + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + String result = "Hotel booked successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java new file mode 100644 index 0000000000..372783353d --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.WorkflowInstanceStatus; + +import java.time.Duration; +import java.util.concurrent.TimeoutException; + +public class BookTripClient { + public static void main(String[] args) { + try (DaprWorkflowClient client = new DaprWorkflowClient()) { + String instanceId = client.scheduleNewWorkflow(BookTripWorkflow.class); + System.out.printf("Started a new trip booking workflow with instance ID: %s%n", instanceId); + + WorkflowInstanceStatus status = client.waitForInstanceCompletion(instanceId, Duration.ofMinutes(30), true); + System.out.printf("Workflow instance with ID: %s completed with status: %s%n", instanceId, status); + System.out.printf("Workflow output: %s%n", status.getSerializedOutput()); + } catch (TimeoutException | InterruptedException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java new file mode 100644 index 0000000000..b88a365754 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; + +public class BookTripWorker { + + public static void main(String[] args) throws Exception { + // Register the Workflow with the builder + WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder() + .registerWorkflow(BookTripWorkflow.class) + .registerActivity(BookFlightActivity.class) + .registerActivity(CancelFlightActivity.class) + .registerActivity(BookHotelActivity.class) + .registerActivity(CancelHotelActivity.class) + .registerActivity(BookCarActivity.class) + .registerActivity(CancelCarActivity.class); + + // Build and start the workflow runtime + try (WorkflowRuntime runtime = builder.build()) { + System.out.println("Start workflow runtime"); + runtime.start(); + } + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java new file mode 100644 index 0000000000..b6330b6ca3 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java @@ -0,0 +1,75 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import com.microsoft.durabletask.TaskFailedException; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +public class BookTripWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + List compensations = new ArrayList<>(); + + try { + String flightResult = ctx.callActivity(BookFlightActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Flight booking completed: {}", flightResult); + compensations.add("CancelFlight"); + + String hotelResult = ctx.callActivity(BookHotelActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Hotel booking completed: {}", hotelResult); + compensations.add("CancelHotel"); + + String carResult = ctx.callActivity(BookCarActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Car booking completed: {}", carResult); + compensations.add("CancelCar"); + + String result = String.format("%s, %s, %s", flightResult, hotelResult, carResult); + ctx.getLogger().info("Trip booked successfully: {}", result); + ctx.complete(result); + } catch (TaskFailedException e) { + ctx.getLogger().info("******** executing compensation logic ********"); + ctx.getLogger().error("Activity failed: {}", e.getMessage()); + Collections.reverse(compensations); + for (String compensation : compensations) { + try { + switch (compensation) { + case "CancelCar": + String carCancelResult = ctx.callActivity(CancelCarActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Car cancellation completed: {}", carCancelResult); + break; + case "CancelHotel": + String hotelCancelResult = ctx.callActivity(CancelHotelActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Hotel cancellation completed: {}", hotelCancelResult); + break; + case "CancelFlight": + String flightCancelResult = ctx.callActivity(CancelFlightActivity.class.getName(), null, String.class).await(); + ctx.getLogger().info("Flight cancellation completed: {}", flightCancelResult); + break; + } + } catch (Exception ex) { + ctx.getLogger().error("Error during compensation: {}", ex.getMessage()); + } + } + ctx.complete("Workflow failed, compensation applied"); + } + }; + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java new file mode 100644 index 0000000000..2ecf0f1257 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class CancelCarActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelCarActivity.class); + + @Override + public String run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + // Simulate work + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + String result = "Car canceled successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java new file mode 100644 index 0000000000..74b7fcb757 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class CancelFlightActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelFlightActivity.class); + + @Override + public String run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + // Simulate work + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + String result = "Flight canceled successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java new file mode 100644 index 0000000000..8218eef6c7 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.workflows.compensation; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +public class CancelHotelActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelHotelActivity.class); + + @Override + public String run(WorkflowActivityContext ctx) { + logger.info("Starting Activity: " + ctx.getName()); + + // Simulate work + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + String result = "Hotel canceled successfully"; + logger.info("Activity completed with result: " + result); + return result; + } +} \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md new file mode 100644 index 0000000000..ceec1d9bac --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md @@ -0,0 +1,47 @@ +# Dapr Workflow Compensation Pattern Example + +This example demonstrates how to implement the compensation pattern in Dapr Workflows using Java. The compensation pattern is used to "undo" or "roll back" previously completed steps if a later step fails. + +## Example Overview + +The example simulates a trip booking workflow that books a flight, hotel, and car. If any step fails, the workflow will automatically compensate (cancel) the previously completed bookings in reverse order. + +- `BookTripWorkflow`: The main workflow that orchestrates the booking process +- `BookFlightActivity`/`CancelFlightActivity`: Activities for booking and canceling flights +- `BookHotelActivity`/`CancelHotelActivity`: Activities for booking and canceling hotels +- `BookCarActivity`/`CancelCarActivity`: Activities for booking and canceling cars +- `BookTripWorker`: Registers the workflow and activities with Dapr +- `BookTripClient`: Starts the workflow and waits for completion + +## Running the Example + +1. Make sure you have Dapr installed and initialized: +```bash +dapr init +``` + +2. Start the workflow worker: + +```bash +dapr run --app-id book-trip-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-sdk-examples-1.15.0-SNAPSHOT-exec.jar io.dapr.examples.workflows.compensation.BookTripWorker +``` + +3. In a separate terminal, run the client: + +```bash +java -jar target/dapr-sdk-examples-1.15.0-SNAPSHOT-exec.jar io.dapr.examples.workflows.compensation.BookTripClient +``` + +## Expected Output + +When running the compensating workflow, you should see logs showing the booking process: +``` +//TODO: markdown +``` + +## Key Points + +1. Each successful booking step pushes its compensation action onto a stack +2. If an error occurs, compensations are executed in reverse order +3. The workflow ensures that all resources are properly cleaned up even if the process fails +4. Each activity simulates work with a short delay for demonstration purposes \ No newline at end of file From 1d74fbc852694d738a0c958f21b9197eb77d59aa Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Mon, 12 May 2025 13:56:58 -0500 Subject: [PATCH 02/22] update commands to run + wf id Signed-off-by: Cassandra Coyle --- .../examples/workflows/compensation/BookTripWorkflow.java | 2 +- .../java/io/dapr/examples/workflows/compensation/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java index b6330b6ca3..4aded0e6a8 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java @@ -25,7 +25,7 @@ public class BookTripWorkflow implements Workflow { @Override public WorkflowStub create() { return ctx -> { - ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + ctx.getLogger().info("Starting Workflow: " + ctx.getInstanceId()); List compensations = new ArrayList<>(); try { diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md index ceec1d9bac..d2de01e19a 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md @@ -23,13 +23,13 @@ dapr init 2. Start the workflow worker: ```bash -dapr run --app-id book-trip-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-sdk-examples-1.15.0-SNAPSHOT-exec.jar io.dapr.examples.workflows.compensation.BookTripWorker +dapr run --app-id book-trip-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripWorker ``` 3. In a separate terminal, run the client: ```bash -java -jar target/dapr-sdk-examples-1.15.0-SNAPSHOT-exec.jar io.dapr.examples.workflows.compensation.BookTripClient +java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripClient ``` ## Expected Output From 312c99c631b11904026322c207ba730353493a35 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Mon, 19 May 2025 16:40:10 -0500 Subject: [PATCH 03/22] update readme + add mechanical markdown Signed-off-by: Cassandra Coyle --- .../compensation/BookTripWorkflow.java | 2 +- .../examples/workflows/compensation/README.md | 118 +++++++++++++++--- 2 files changed, 104 insertions(+), 16 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java index 4aded0e6a8..b6330b6ca3 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java @@ -25,7 +25,7 @@ public class BookTripWorkflow implements Workflow { @Override public WorkflowStub create() { return ctx -> { - ctx.getLogger().info("Starting Workflow: " + ctx.getInstanceId()); + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); List compensations = new ArrayList<>(); try { diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md index d2de01e19a..8a41b7d6fe 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md @@ -15,33 +15,121 @@ The example simulates a trip booking workflow that books a flight, hotel, and ca ## Running the Example -1. Make sure you have Dapr installed and initialized: -```bash -dapr init +### Checking out the code + +Clone this repository, if you don't already have it cloned: + +```sh +git clone https://github.com/dapr/java-sdk.git +cd java-sdk ``` -2. Start the workflow worker: +Then build the Maven project: -```bash -dapr run --app-id book-trip-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripWorker +```sh +# make sure you are in the `java-sdk` directory. +mvn install +``` + +Get into the `examples` directory. +```sh +cd examples ``` -3. In a separate terminal, run the client: +### Initialize Dapr -```bash -java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripClient +Run `dapr init` to initialize Dapr in Self-Hosted Mode if it's not already initialized. + +### Compensation Example + +When running the compensating workflow, you should see logs showing the booking process and compensation: + + + +Execute the following script in order to run the BookTripWorker: +```sh +dapr run --app-id book-trip-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripWorker ``` -## Expected Output +Once running, you can see the registered workflow and activities followed by the durabletask grpc worker starting: +```text +== APP == 2025-05-19 16:09:51,487 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Workflow: BookTripWorkflow +== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: BookFlightActivity +== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelFlightActivity +== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: BookHotelActivity +== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelHotelActivity +== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: BookCarActivity +== APP == 2025-05-19 16:09:51,491 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelCarActivity +== APP == 2025-05-19 16:09:51,619 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - List of registered workflows: [io.dapr.examples.workflows.compensation.BookTripWorkflow] +== APP == 2025-05-19 16:09:51,619 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - List of registered activities: [io.dapr.examples.workflows.compensation.BookCarActivity, io.dapr.examples.workflows.compensation.BookFlightActivity, io.dapr.examples.workflows.compensation.CancelHotelActivity, io.dapr.examples.workflows.compensation.CancelCarActivity, io.dapr.examples.workflows.compensation.CancelFlightActivity, io.dapr.examples.workflows.compensation.BookHotelActivity] +== APP == 2025-05-19 16:09:51,619 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Successfully built dapr workflow runtime +== APP == Start workflow runtime +== APP == May 19, 2025 4:09:51 PM io.dapr.durabletask.DurableTaskGrpcWorker startAndBlock +== APP == INFO: Durable Task worker is connecting to sidecar at 127.0.0.1:50001. +``` -When running the compensating workflow, you should see logs showing the booking process: +Once running, execute the following script to run the BookTripClient: +```sh +java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripClient ``` -//TODO: markdown + + +The client logs show the workflow being started, then completes with `RuntimeStatus: COMPLETED` and output showing `Workflow failed, compensation applied`, meaning that our workflow compensation applied because we forced an error in the book car activity: +```text +Started a new trip booking workflow with instance ID: aec453a6-8055-46dd-a976-f41a38d13558 +Workflow instance with ID: aec453a6-8055-46dd-a976-f41a38d13558 completed with status: [Name: 'io.dapr.examples.workflows.compensation.BookTripWorkflow', ID: 'aec453a6-8055-46dd-a976-f41a38d13558', RuntimeStatus: COMPLETED, CreatedAt: 2025-05-19T20:51:28.714Z, LastUpdatedAt: 2025-05-19T20:51:34.876Z, Input: '', Output: '"Workflow failed, compensation applied"'] +Workflow output: "Workflow failed, compensation applied" ``` +The output demonstrates: +1. The workflow starts and successfully books a flight +2. Then successfully books a hotel +3. When attempting to book a car, it fails (intentionally) +4. The compensation logic triggers, canceling the hotel and flight in reverse order +5. The workflow completes with a status indicating the compensation was applied + ## Key Points -1. Each successful booking step pushes its compensation action onto a stack -2. If an error occurs, compensations are executed in reverse order +1. Each successful booking step adds its compensation action to an ArrayList +2. If an error occurs, the list of compensations is reversed and executed in reverse order 3. The workflow ensures that all resources are properly cleaned up even if the process fails -4. Each activity simulates work with a short delay for demonstration purposes \ No newline at end of file +4. Each activity simulates work with a short delay for demonstration purposes \ No newline at end of file From 417a0af99bf31d206fb07f1a9745c8e450244aac Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Mon, 19 May 2025 16:44:31 -0500 Subject: [PATCH 04/22] fix import Signed-off-by: Cassandra Coyle --- .../dapr/examples/workflows/compensation/BookTripWorkflow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java index b6330b6ca3..615ba3bb56 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java @@ -13,7 +13,7 @@ package io.dapr.examples.workflows.compensation; -import com.microsoft.durabletask.TaskFailedException; +import io.dapr.durabletask.TaskFailedException; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; From 3f8cab00439c821de4b0aebe716a335b1d522969 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Mon, 19 May 2025 17:24:23 -0500 Subject: [PATCH 05/22] fix mechanical markdown + add how to test it locally Signed-off-by: Cassandra Coyle --- CONTRIBUTING.md | 43 ++++++++++++- .../examples/workflows/compensation/README.md | 61 +++++++++---------- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c094f92fa1..e6b6d6d452 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,48 @@ This section describes the guidelines for contributing code / docs to Dapr. ### Things to consider when adding new API to SDK 1. All the new API's go under [dapr-sdk maven package](https://github.com/dapr/java-sdk/tree/master/sdk) -2. Make sure there is an example talking about how to use the API along with a README. [Example](https://github.com/dapr/java-sdk/pull/1235/files#diff-69ed756c4c01fd5fa884aac030dccb8f3f4d4fefa0dc330862d55a6f87b34a14) +2. Make sure there is an example talking about how to use the API along with a README with mechanical markdown. [Example](https://github.com/dapr/java-sdk/pull/1235/files#diff-69ed756c4c01fd5fa884aac030dccb8f3f4d4fefa0dc330862d55a6f87b34a14) + +#### Mechanical Markdown + +Mechanical markdown is used to validate example outputs in our CI pipeline. It ensures that the expected output in README files matches the actual output when running the examples. This helps maintain example output, catches any unintended changes in example behavior, and regressions. + +To test mechanical markdown locally: + +1. Install the package: +```bash +pip3 install mechanical-markdown +``` + +2. Run the test from the respective examples README directory, for example: +```bash +cd examples +mm.py ./src/main/java/io/dapr/examples/workflows/compensation/README.md +``` + +The test will: +- Parse the STEP markers in the README +- Execute the commands specified in the markers +- Compare the actual output with the expected output +- Report any mismatches + +When writing STEP markers: +- Use `output_match_mode: substring` for flexible matching +- Quote strings containing special YAML characters (like `:`, `*`, `'`) +- Set appropriate timeouts for long-running examples + +Example STEP marker: +```yaml + +``` ### Pull Requests diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md index 8a41b7d6fe..9d5bafc130 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md @@ -49,37 +49,34 @@ name: Run Compensation Pattern workflow worker match_order: none output_match_mode: substring expected_stdout_lines: - - Registered Workflow: BookTripWorkflow - - Registered Activity: BookFlightActivity - - Registered Activity: CancelFlightActivity - - Registered Activity: BookHotelActivity - - Registered Activity: CancelHotelActivity - - Registered Activity: BookCarActivity - - Registered Activity: CancelCarActivity - - List of registered workflows: [io.dapr.examples.workflows.compensation.BookTripWorkflow] - - List of registered activities: [io.dapr.examples.workflows.compensation.BookCarActivity, io.dapr.examples.workflows.compensation.BookFlightActivity, io.dapr.examples.workflows.compensation.CancelHotelActivity, io.dapr.examples.workflows.compensation.CancelCarActivity, io.dapr.examples.workflows.compensation.CancelFlightActivity, io.dapr.examples.workflows.compensation.BookHotelActivity] - - Successfully built dapr workflow runtime - - Start workflow runtime - - io.dapr.durabletask.DurableTaskGrpcWorker startAndBlock - - Durable Task worker is connecting to sidecar at 127.0.0.1:50001. - - - Starting Workflow: io.dapr.examples.workflows.compensation.BookTripWorkflow - - Starting Activity: io.dapr.examples.workflows.compensation.BookFlightActivity - - Activity completed with result: Flight booked successfully - - Flight booking completed: Flight booked successfully - - Starting Activity: io.dapr.examples.workflows.compensation.BookHotelActivity - - Simulating hotel booking process... - - Activity completed with result: Hotel booked successfully - - Hotel booking completed: Hotel booked successfully - - Starting Activity: io.dapr.examples.workflows.compensation.BookCarActivity - - Forcing Failure to trigger compensation for activity: io.dapr.examples.workflows.compensation.BookCarActivity - - ******** executing compensation logic ******** - - Activity failed: Task 'io.dapr.examples.workflows.compensation.BookCarActivity' (#2) failed with an unhandled exception: Failed to book car - - Error during compensation: The orchestrator is blocked and waiting for new inputs. This Throwable should never be caught by user code. - - Starting Activity: io.dapr.examples.workflows.compensation.CancelHotelActivity - - Activity completed with result: Hotel canceled successfully - - Starting Activity: io.dapr.examples.workflows.compensation.CancelFlightActivity - - Activity completed with result: Flight canceled successfully + - "Registered Workflow: BookTripWorkflow" + - "Registered Activity: BookFlightActivity" + - "Registered Activity: CancelFlightActivity" + - "Registered Activity: BookHotelActivity" + - "Registered Activity: CancelHotelActivity" + - "Registered Activity: BookCarActivity" + - "Registered Activity: CancelCarActivity" + - "Successfully built dapr workflow runtime" + - "Start workflow runtime" + - "Durable Task worker is connecting to sidecar at 127.0.0.1:50001." + + - "Starting Workflow: io.dapr.examples.workflows.compensation.BookTripWorkflow" + - "Starting Activity: io.dapr.examples.workflows.compensation.BookFlightActivity" + - "Activity completed with result: Flight booked successfully" + - "Flight booking completed: Flight booked successfully" + - "Starting Activity: io.dapr.examples.workflows.compensation.BookHotelActivity" + - "Simulating hotel booking process..." + - "Activity completed with result: Hotel booked successfully" + - "Hotel booking completed: Hotel booked successfully" + - "Starting Activity: io.dapr.examples.workflows.compensation.BookCarActivity" + - "Forcing Failure to trigger compensation for activity: io.dapr.examples.workflows.compensation.BookCarActivity" + - "******** executing compensation logic ********" + - "Activity failed: Task 'io.dapr.examples.workflows.compensation.BookCarActivity' (#2) failed with an unhandled exception: Failed to book car" + - "Error during compensation: The orchestrator is blocked and waiting for new inputs. This Throwable should never be caught by user code." + - "Starting Activity: io.dapr.examples.workflows.compensation.CancelHotelActivity" + - "Activity completed with result: Hotel canceled successfully" + - "Starting Activity: io.dapr.examples.workflows.compensation.CancelFlightActivity" + - "Activity completed with result: Flight canceled successfully" background: true sleep: 60 timeout_seconds: 60 @@ -99,8 +96,6 @@ Once running, you can see the registered workflow and activities followed by the == APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelHotelActivity == APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: BookCarActivity == APP == 2025-05-19 16:09:51,491 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelCarActivity -== APP == 2025-05-19 16:09:51,619 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - List of registered workflows: [io.dapr.examples.workflows.compensation.BookTripWorkflow] -== APP == 2025-05-19 16:09:51,619 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - List of registered activities: [io.dapr.examples.workflows.compensation.BookCarActivity, io.dapr.examples.workflows.compensation.BookFlightActivity, io.dapr.examples.workflows.compensation.CancelHotelActivity, io.dapr.examples.workflows.compensation.CancelCarActivity, io.dapr.examples.workflows.compensation.CancelFlightActivity, io.dapr.examples.workflows.compensation.BookHotelActivity] == APP == 2025-05-19 16:09:51,619 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Successfully built dapr workflow runtime == APP == Start workflow runtime == APP == May 19, 2025 4:09:51 PM io.dapr.durabletask.DurableTaskGrpcWorker startAndBlock From 457dd76339ac3940bbb268b5105f227eb92a779d Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Mon, 19 May 2025 17:30:40 -0500 Subject: [PATCH 06/22] move compensation example readme to workflows readme Signed-off-by: Cassandra Coyle --- CONTRIBUTING.md | 2 +- .../java/io/dapr/examples/workflows/README.md | 120 +++++++++++++++- .../examples/workflows/compensation/README.md | 130 ------------------ 3 files changed, 118 insertions(+), 134 deletions(-) delete mode 100644 examples/src/main/java/io/dapr/examples/workflows/compensation/README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6b6d6d452..06c9117027 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,7 +70,7 @@ pip3 install mechanical-markdown 2. Run the test from the respective examples README directory, for example: ```bash cd examples -mm.py ./src/main/java/io/dapr/examples/workflows/compensation/README.md +mm.py ./src/main/java/io/dapr/examples/workflows/README.md ``` The test will: diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 047f1d744f..4fdec23657 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -51,7 +51,8 @@ Those examples contain the following workflow patterns: 2. [Fan-out/Fan-in Pattern](#fan-outfan-in-pattern) 3. [Continue As New Pattern](#continue-as-new-pattern) 4. [External Event Pattern](#external-event-pattern) -5. [child-workflow Pattern](#child-workflow-pattern) +5. [Child-workflow Pattern](#child-workflow-pattern) +6. [Compensation Pattern](#compensation-pattern) ### Chaining Pattern In the chaining pattern, a sequence of activities executes in a specific order. @@ -353,7 +354,7 @@ dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- ``` ```sh java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.continueasnew.DemoContinueAsNewClient -```` +``` You will see the logs from worker showing the `CleanUpActivity` is invoked every 10 seconds after previous one is finished: ```text @@ -444,7 +445,7 @@ Started a new external-event model workflow with instance ID: 23410d96-1afe-4698 workflow instance with ID: 23410d96-1afe-4698-9fcd-c01c1e0db255 completed. ``` -### child-workflow Pattern +### Child-workflow Pattern The child-workflow pattern allows you to call a workflow from another workflow. The `DemoWorkflow` class defines the workflow. It calls a child-workflow `DemoChildWorkflow` to do the work. See the code snippet below: @@ -540,3 +541,116 @@ The log from client: Started a new child-workflow model workflow with instance ID: c2fb9c83-435b-4b55-bdf1-833b39366cfb workflow instance with ID: c2fb9c83-435b-4b55-bdf1-833b39366cfb completed with result: !wolfkroW rpaD olleH ``` + +### Compensation Pattern +The compensation pattern is used to "undo" or "roll back" previously completed steps if a later step fails. This pattern is particularly useful in scenarios where you need to ensure that all resources are properly cleaned up even if the process fails. + +The example simulates a trip booking workflow that books a flight, hotel, and car. If any step fails, the workflow will automatically compensate (cancel) the previously completed bookings in reverse order. + +The `BookTripWorkflow` class defines the workflow. It orchestrates the booking process and handles compensation if any step fails. See the code snippet below: +```java +public class BookTripWorkflow extends Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + List compensations = new ArrayList<>(); + + try { + // Book flight + String flightResult = ctx.callActivity(BookFlightActivity.class.getName(), String.class).await(); + ctx.getLogger().info("Flight booking completed: " + flightResult); + compensations.add(CancelFlightActivity.class.getName()); + + // Book hotel + String hotelResult = ctx.callActivity(BookHotelActivity.class.getName(), String.class).await(); + ctx.getLogger().info("Hotel booking completed: " + hotelResult); + compensations.add(CancelHotelActivity.class.getName()); + + // Book car + String carResult = ctx.callActivity(BookCarActivity.class.getName(), String.class).await(); + ctx.getLogger().info("Car booking completed: " + carResult); + compensations.add(CancelCarActivity.class.getName()); + + } catch (Exception e) { + ctx.getLogger().info("******** executing compensation logic ********"); + // Execute compensations in reverse order + Collections.reverse(compensations); + for (String compensation : compensations) { + try { + ctx.callActivity(compensation, String.class).await(); + } catch (Exception ex) { + ctx.getLogger().error("Error during compensation: " + ex.getMessage()); + } + } + ctx.complete("Workflow failed, compensation applied"); + return; + } + ctx.complete("All bookings completed successfully"); + }; + } +} +``` + +Each activity class (`BookFlightActivity`, `BookHotelActivity`, `BookCarActivity`) implements the booking logic, while their corresponding compensation activities (`CancelFlightActivity`, `CancelHotelActivity`, `CancelCarActivity`) implement the cancellation logic. + + + +Execute the following script in order to run the BookTripWorker: +```sh +dapr run --app-id book-trip-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripWorker +``` + +Once running, execute the following script to run the BookTripClient: +```sh +java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripClient +``` + + +The output demonstrates: +1. The workflow starts and successfully books a flight +2. Then successfully books a hotel +3. When attempting to book a car, it fails (intentionally) +4. The compensation logic triggers, canceling the hotel and flight in reverse order +5. The workflow completes with a status indicating the compensation was applied + +Key Points: +1. Each successful booking step adds its compensation action to an ArrayList +2. If an error occurs, the list of compensations is reversed and executed in reverse order +3. The workflow ensures that all resources are properly cleaned up even if the process fails +4. Each activity simulates work with a short delay for demonstration purposes \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md b/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md deleted file mode 100644 index 9d5bafc130..0000000000 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Dapr Workflow Compensation Pattern Example - -This example demonstrates how to implement the compensation pattern in Dapr Workflows using Java. The compensation pattern is used to "undo" or "roll back" previously completed steps if a later step fails. - -## Example Overview - -The example simulates a trip booking workflow that books a flight, hotel, and car. If any step fails, the workflow will automatically compensate (cancel) the previously completed bookings in reverse order. - -- `BookTripWorkflow`: The main workflow that orchestrates the booking process -- `BookFlightActivity`/`CancelFlightActivity`: Activities for booking and canceling flights -- `BookHotelActivity`/`CancelHotelActivity`: Activities for booking and canceling hotels -- `BookCarActivity`/`CancelCarActivity`: Activities for booking and canceling cars -- `BookTripWorker`: Registers the workflow and activities with Dapr -- `BookTripClient`: Starts the workflow and waits for completion - -## Running the Example - -### Checking out the code - -Clone this repository, if you don't already have it cloned: - -```sh -git clone https://github.com/dapr/java-sdk.git -cd java-sdk -``` - -Then build the Maven project: - -```sh -# make sure you are in the `java-sdk` directory. -mvn install -``` - -Get into the `examples` directory. -```sh -cd examples -``` - -### Initialize Dapr - -Run `dapr init` to initialize Dapr in Self-Hosted Mode if it's not already initialized. - -### Compensation Example - -When running the compensating workflow, you should see logs showing the booking process and compensation: - - - -Execute the following script in order to run the BookTripWorker: -```sh -dapr run --app-id book-trip-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripWorker -``` - -Once running, you can see the registered workflow and activities followed by the durabletask grpc worker starting: -```text -== APP == 2025-05-19 16:09:51,487 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Workflow: BookTripWorkflow -== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: BookFlightActivity -== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelFlightActivity -== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: BookHotelActivity -== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelHotelActivity -== APP == 2025-05-19 16:09:51,490 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: BookCarActivity -== APP == 2025-05-19 16:09:51,491 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Registered Activity: CancelCarActivity -== APP == 2025-05-19 16:09:51,619 {HH:mm:ss.SSS} [main] INFO i.d.w.runtime.WorkflowRuntimeBuilder - Successfully built dapr workflow runtime -== APP == Start workflow runtime -== APP == May 19, 2025 4:09:51 PM io.dapr.durabletask.DurableTaskGrpcWorker startAndBlock -== APP == INFO: Durable Task worker is connecting to sidecar at 127.0.0.1:50001. -``` - -Once running, execute the following script to run the BookTripClient: -```sh -java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripClient -``` - - -The client logs show the workflow being started, then completes with `RuntimeStatus: COMPLETED` and output showing `Workflow failed, compensation applied`, meaning that our workflow compensation applied because we forced an error in the book car activity: -```text -Started a new trip booking workflow with instance ID: aec453a6-8055-46dd-a976-f41a38d13558 -Workflow instance with ID: aec453a6-8055-46dd-a976-f41a38d13558 completed with status: [Name: 'io.dapr.examples.workflows.compensation.BookTripWorkflow', ID: 'aec453a6-8055-46dd-a976-f41a38d13558', RuntimeStatus: COMPLETED, CreatedAt: 2025-05-19T20:51:28.714Z, LastUpdatedAt: 2025-05-19T20:51:34.876Z, Input: '', Output: '"Workflow failed, compensation applied"'] -Workflow output: "Workflow failed, compensation applied" -``` - -The output demonstrates: -1. The workflow starts and successfully books a flight -2. Then successfully books a hotel -3. When attempting to book a car, it fails (intentionally) -4. The compensation logic triggers, canceling the hotel and flight in reverse order -5. The workflow completes with a status indicating the compensation was applied - -## Key Points - -1. Each successful booking step adds its compensation action to an ArrayList -2. If an error occurs, the list of compensations is reversed and executed in reverse order -3. The workflow ensures that all resources are properly cleaned up even if the process fails -4. Each activity simulates work with a short delay for demonstration purposes \ No newline at end of file From b5829bf2e744411b0214c5182e07c49c428ca4ee Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:11:10 +0300 Subject: [PATCH 07/22] Update BookCarActivity.java Signed-off-by: artur-ciocanu --- .../dapr/examples/workflows/compensation/BookCarActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java index e96f854a70..9ad0285d93 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookCarActivity.java @@ -39,4 +39,4 @@ public String run(WorkflowActivityContext ctx) { // force the compensation throw new RuntimeException("Failed to book car"); } -} \ No newline at end of file +} From 94ed1a9a59a012d7b7486b5dd6bbab62e97c1723 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:11:35 +0300 Subject: [PATCH 08/22] Update BookFlightActivity.java Signed-off-by: artur-ciocanu --- .../examples/workflows/compensation/BookFlightActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java index 0f76a498c0..075c4d275f 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookFlightActivity.java @@ -38,4 +38,4 @@ public String run(WorkflowActivityContext ctx) { logger.info("Activity completed with result: " + result); return result; } -} \ No newline at end of file +} From 5eba62644e94e616b132e1d9cc776e9ee65191e2 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:12:02 +0300 Subject: [PATCH 09/22] Update BookHotelActivity.java Signed-off-by: artur-ciocanu --- .../dapr/examples/workflows/compensation/BookHotelActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java index 307c9c14cb..a2eca04c45 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookHotelActivity.java @@ -37,4 +37,4 @@ public String run(WorkflowActivityContext ctx) { logger.info("Activity completed with result: " + result); return result; } -} \ No newline at end of file +} From 5f227ad8c789cfe71f2c242c6979c3e50c92b71a Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:12:26 +0300 Subject: [PATCH 10/22] Update BookTripClient.java Signed-off-by: artur-ciocanu --- .../io/dapr/examples/workflows/compensation/BookTripClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java index 372783353d..212c1f0a1e 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripClient.java @@ -32,4 +32,4 @@ public static void main(String[] args) { throw new RuntimeException(e); } } -} \ No newline at end of file +} From 2e4448b6558f1e4357b895fba524fe6a0d8e4852 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:12:50 +0300 Subject: [PATCH 11/22] Update BookTripWorker.java Signed-off-by: artur-ciocanu --- .../io/dapr/examples/workflows/compensation/BookTripWorker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java index b88a365754..d32ade26a1 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorker.java @@ -35,4 +35,4 @@ public static void main(String[] args) throws Exception { runtime.start(); } } -} \ No newline at end of file +} From 4beb4586334601b68d089e35fc8efbc206a81102 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:13:20 +0300 Subject: [PATCH 12/22] Update BookTripWorkflow.java Signed-off-by: artur-ciocanu --- .../dapr/examples/workflows/compensation/BookTripWorkflow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java index 615ba3bb56..84462420a6 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java @@ -72,4 +72,4 @@ public WorkflowStub create() { } }; } -} \ No newline at end of file +} From e272c1432f7e095337bc9404108bef5974ef58a5 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:13:54 +0300 Subject: [PATCH 13/22] Update CancelCarActivity.java Signed-off-by: artur-ciocanu --- .../dapr/examples/workflows/compensation/CancelCarActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java index 2ecf0f1257..bca6af0da1 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelCarActivity.java @@ -38,4 +38,4 @@ public String run(WorkflowActivityContext ctx) { logger.info("Activity completed with result: " + result); return result; } -} \ No newline at end of file +} From cd80b1a7f29af63c34514223ae7bfa3206b37a7c Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:14:22 +0300 Subject: [PATCH 14/22] Update CancelFlightActivity.java Signed-off-by: artur-ciocanu --- .../examples/workflows/compensation/CancelFlightActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java index 74b7fcb757..0c2034dee9 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelFlightActivity.java @@ -38,4 +38,4 @@ public String run(WorkflowActivityContext ctx) { logger.info("Activity completed with result: " + result); return result; } -} \ No newline at end of file +} From 5dbf3dc7942a7bbfaab78241982856f38110fa63 Mon Sep 17 00:00:00 2001 From: artur-ciocanu Date: Thu, 22 May 2025 18:14:46 +0300 Subject: [PATCH 15/22] Update CancelHotelActivity.java Signed-off-by: artur-ciocanu --- .../examples/workflows/compensation/CancelHotelActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java index 8218eef6c7..03f5f9b645 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/CancelHotelActivity.java @@ -38,4 +38,4 @@ public String run(WorkflowActivityContext ctx) { logger.info("Activity completed with result: " + result); return result; } -} \ No newline at end of file +} From 732d4f3e53f2d754e25cf6f80c5f8feff24a4150 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 29 May 2025 09:52:29 -0500 Subject: [PATCH 16/22] add retry IT tests and catch TaskFailedException Signed-off-by: Cassandra Coyle --- .../compensation/BookTripWorkflow.java | 42 +- .../it/testcontainers/DaprWorkflowsIT.java | 1 - .../TestDaprWorkflowsConfiguration.java | 11 + .../WorkflowRetryCompensationIT.java | 422 ++++++++++++++++++ .../it/testcontainers/WorkflowRetryIT.java | 202 +++++++++ 5 files changed, 672 insertions(+), 6 deletions(-) create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java diff --git a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java index 84462420a6..f375363edd 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/compensation/BookTripWorkflow.java @@ -16,10 +16,13 @@ import io.dapr.durabletask.TaskFailedException; import io.dapr.workflows.Workflow; import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; import java.util.List; import java.util.ArrayList; import java.util.Collections; +import java.time.Duration; public class BookTripWorkflow implements Workflow { @Override @@ -28,15 +31,26 @@ public WorkflowStub create() { ctx.getLogger().info("Starting Workflow: " + ctx.getName()); List compensations = new ArrayList<>(); + // Define retry policy for compensation activities + WorkflowTaskRetryPolicy compensationRetryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setFirstRetryInterval(Duration.ofSeconds(1)) + .setMaxNumberOfAttempts(3) + .build(); + + WorkflowTaskOptions compensationOptions = new WorkflowTaskOptions(compensationRetryPolicy); + try { + // Book flight String flightResult = ctx.callActivity(BookFlightActivity.class.getName(), null, String.class).await(); ctx.getLogger().info("Flight booking completed: {}", flightResult); compensations.add("CancelFlight"); + // Book hotel String hotelResult = ctx.callActivity(BookHotelActivity.class.getName(), null, String.class).await(); ctx.getLogger().info("Hotel booking completed: {}", hotelResult); compensations.add("CancelHotel"); + // Book car String carResult = ctx.callActivity(BookCarActivity.class.getName(), null, String.class).await(); ctx.getLogger().info("Car booking completed: {}", carResult); compensations.add("CancelCar"); @@ -44,28 +58,46 @@ public WorkflowStub create() { String result = String.format("%s, %s, %s", flightResult, hotelResult, carResult); ctx.getLogger().info("Trip booked successfully: {}", result); ctx.complete(result); + } catch (TaskFailedException e) { ctx.getLogger().info("******** executing compensation logic ********"); ctx.getLogger().error("Activity failed: {}", e.getMessage()); + + // Execute compensations in reverse order Collections.reverse(compensations); for (String compensation : compensations) { try { switch (compensation) { case "CancelCar": - String carCancelResult = ctx.callActivity(CancelCarActivity.class.getName(), null, String.class).await(); + String carCancelResult = ctx.callActivity( + CancelCarActivity.class.getName(), + null, + compensationOptions, + String.class).await(); ctx.getLogger().info("Car cancellation completed: {}", carCancelResult); break; + case "CancelHotel": - String hotelCancelResult = ctx.callActivity(CancelHotelActivity.class.getName(), null, String.class).await(); + String hotelCancelResult = ctx.callActivity( + CancelHotelActivity.class.getName(), + null, + compensationOptions, + String.class).await(); ctx.getLogger().info("Hotel cancellation completed: {}", hotelCancelResult); break; + case "CancelFlight": - String flightCancelResult = ctx.callActivity(CancelFlightActivity.class.getName(), null, String.class).await(); + String flightCancelResult = ctx.callActivity( + CancelFlightActivity.class.getName(), + null, + compensationOptions, + String.class).await(); ctx.getLogger().info("Flight cancellation completed: {}", flightCancelResult); break; } - } catch (Exception ex) { - ctx.getLogger().error("Error during compensation: {}", ex.getMessage()); + } catch (TaskFailedException ex) { + // Only catch TaskFailedException for actual activity failures + ctx.getLogger().error("Activity failed during compensation: {}", ex.getMessage()); } } ctx.complete("Workflow failed, compensation applied"); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java index 5c6a360c8a..66f86a2c2b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java @@ -62,7 +62,6 @@ public class DaprWorkflowsIT { .withNetwork(DAPR_NETWORK) .withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true"))) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withDaprLogLevel(DaprLogLevel.DEBUG) .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .withAppChannelAddress("host.testcontainers.internal"); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java index 0a2487b70c..c1417ebbc5 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java @@ -59,6 +59,17 @@ public WorkflowRuntimeBuilder workflowRuntimeBuilder( builder.registerActivity(FirstActivity.class); builder.registerActivity(SecondActivity.class); + builder.registerWorkflow(WorkflowRetryIT.RetryTestWorkflowImpl.class); + builder.registerActivity(WorkflowRetryIT.RetryTestActivity.class); + + builder.registerWorkflow(WorkflowRetryCompensationIT.BookTripWorkflow.class); + builder.registerActivity(WorkflowRetryCompensationIT.BookFlightActivity.class); + builder.registerActivity(WorkflowRetryCompensationIT.BookHotelActivity.class); + builder.registerActivity(WorkflowRetryCompensationIT.BookCarActivity.class); + builder.registerActivity(WorkflowRetryCompensationIT.CancelFlightActivity.class); + builder.registerActivity(WorkflowRetryCompensationIT.CancelHotelActivity.class); + builder.registerActivity(WorkflowRetryCompensationIT.CancelCarActivity.class); + return builder; } } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java new file mode 100644 index 0000000000..9807c4d1cb --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java @@ -0,0 +1,422 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it.testcontainers; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.WorkflowInstanceStatus; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.client.WorkflowFailureDetails; +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.dapr.durabletask.TaskFailedException; + +@SpringBootTest( + webEnvironment = WebEnvironment.RANDOM_PORT, + classes = { + TestDaprWorkflowsConfiguration.class, + TestWorkflowsApplication.class + } +) +@Testcontainers +@Tag("testcontainers") +public class WorkflowRetryCompensationIT { + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("workflow-retry-compensation-app") + .withNetwork(DAPR_NETWORK) + .withComponent(new Component("kvstore", "state.in-memory", "v1", + Map.of("actorStateStore", "true"))) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withAppChannelAddress("host.testcontainers.internal"); + + @DynamicPropertySource + static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); + registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); + } + + @Autowired + private DaprWorkflowClient workflowClient; + + @Autowired + private WorkflowRuntimeBuilder workflowRuntimeBuilder; + + private WorkflowRuntime runtime; + + @BeforeEach + public void init() { + // Reset attempt counts before each test + BookFlightActivity.attemptCount = 0; + BookHotelActivity.attemptCount = 0; + BookCarActivity.attemptCount = 0; + CancelFlightActivity.attemptCount = 0; + CancelHotelActivity.attemptCount = 0; + CancelCarActivity.attemptCount = 0; + + // Reset failure flags + BookCarActivity.alwaysFail = false; + CancelHotelActivity.shouldFail = false; + + runtime = workflowRuntimeBuilder.build(); + System.out.println("Starting new workflow runtime for test"); + runtime.start(false); + } + + public static class WorkflowInput { + private boolean failCarBooking; + private boolean failHotelCancellation; + + // Default constructor + public WorkflowInput() { + this.failCarBooking = false; + this.failHotelCancellation = false; + } + + public WorkflowInput(boolean failCarBooking, boolean failHotelCancellation) { + this.failCarBooking = failCarBooking; + this.failHotelCancellation = failHotelCancellation; + } + + public boolean isFailCarBooking() { + return failCarBooking; + } + + public void setFailCarBooking(boolean failCarBooking) { + this.failCarBooking = failCarBooking; + } + + public boolean isFailHotelCancellation() { + return failHotelCancellation; + } + + public void setFailHotelCancellation(boolean failHotelCancellation) { + this.failHotelCancellation = failHotelCancellation; + } + } + + public static class BookTripWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting BookTripWorkflow"); + List compensations = new ArrayList<>(); + + // Get workflow input + WorkflowInput input = ctx.getInput(WorkflowInput.class); + ctx.getLogger().info("Workflow input: failCarBooking={}, failHotelCancellation={}", + input.isFailCarBooking(), input.isFailHotelCancellation()); + + WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(3) + .setFirstRetryInterval(Duration.ofSeconds(1)) + .setRetryTimeout(Duration.ofSeconds(30)) + .build(); + + WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy); + + try { + // Book flight (should succeed) + ctx.getLogger().info("Attempting to book flight..."); + String flightResult = ctx.callActivity( + BookFlightActivity.class.getCanonicalName(), + null, + options, + String.class).await(); + ctx.getLogger().info("Flight booking completed: {}", flightResult); + compensations.add("CancelFlight"); + ctx.getLogger().info("Added flight cancellation to compensation list. Current compensations: {}", compensations); + + // Book hotel (should succeed) + ctx.getLogger().info("Attempting to book hotel..."); + String hotelResult = ctx.callActivity( + BookHotelActivity.class.getCanonicalName(), + null, + options, + String.class).await(); + ctx.getLogger().info("Hotel booking completed: {}", hotelResult); + compensations.add("CancelHotel"); + ctx.getLogger().info("Added hotel cancellation to compensation list. Current compensations: {}", compensations); + + // Book car (should fail if configured) + ctx.getLogger().info("Attempting to book car with shouldFail={}...", input.isFailCarBooking()); + String carResult = ctx.callActivity( + BookCarActivity.class.getCanonicalName(), + null, + options, + String.class).await(); + ctx.getLogger().info("Car booking completed: {}", carResult); + compensations.add("CancelCar"); + ctx.getLogger().info("Added car cancellation to compensation list. Current compensations: {}", compensations); + + String result = String.format("%s, %s, %s", flightResult, hotelResult, carResult); + ctx.getLogger().info("Trip booked successfully: {}", result); + ctx.complete(result); + + } catch (TaskFailedException e) { + + ctx.getLogger().error("Activity failed: {}", e.getMessage()); + ctx.getLogger().info("******** executing compensation logic ********"); + ctx.getLogger().info("Compensation list before reversal: {}", compensations); + + // Execute compensations in reverse order + Collections.reverse(compensations); + ctx.getLogger().info("Compensation list after reversal: {}", compensations); + + for (String compensation : compensations) { + try { + ctx.getLogger().info("Executing compensation: {}", compensation); + switch (compensation) { + case "CancelCar": + ctx.getLogger().info("Calling CancelCarActivity..."); + String carCancelResult = ctx.callActivity( + CancelCarActivity.class.getCanonicalName(), + null, + options, + String.class).await(); + ctx.getLogger().info("Car cancellation completed: {}", carCancelResult); + break; + + case "CancelHotel": + ctx.getLogger().info("Calling CancelHotelActivity with shouldFail={}...", input.isFailHotelCancellation()); + String hotelCancelResult = ctx.callActivity( + CancelHotelActivity.class.getCanonicalName(), + null, // No input needed, use static flag + options, + String.class).await(); + ctx.getLogger().info("Hotel cancellation completed: {}", hotelCancelResult); + break; + + case "CancelFlight": + ctx.getLogger().info("Calling CancelFlightActivity..."); + String flightCancelResult = ctx.callActivity( + CancelFlightActivity.class.getCanonicalName(), + null, + options, + String.class).await(); + ctx.getLogger().info("Flight cancellation completed: {}", flightCancelResult); + break; + } + } catch (TaskFailedException ex) { + ctx.getLogger().error("Compensation activity {} failed: {}", compensation, ex.getMessage()); + } + } + ctx.getLogger().info("All compensations executed. Completing workflow."); + ctx.complete("Workflow failed, compensation applied"); + } + }; + } + } + + public static class BookFlightActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookFlightActivity.class); + private static int attemptCount = 0; + + @Override + public Object run(WorkflowActivityContext ctx) { + attemptCount++; + logger.info("BookFlightActivity attempt #{}", attemptCount); + return "Flight booked successfully"; + } + } + + public static class BookHotelActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookHotelActivity.class); + private static int attemptCount = 0; + + @Override + public Object run(WorkflowActivityContext ctx) { + attemptCount++; + logger.info("BookHotelActivity attempt #{}", attemptCount); + return "Hotel booked successfully"; + } + } + + public static class BookCarActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(BookCarActivity.class); + private static int attemptCount = 0; + private static boolean alwaysFail = false; + + @Override + public Object run(WorkflowActivityContext ctx) { + attemptCount++; + logger.info("BookCarActivity attempt #{} (alwaysFail={})", attemptCount, alwaysFail); + + if (alwaysFail) { + String errorMsg = String.format("Car booking failed on attempt %d (alwaysFail=true)", attemptCount); + logger.info("BookCarActivity failing: {}", errorMsg); + throw new RuntimeException(errorMsg); + } + + if (attemptCount < 3) { + String errorMsg = String.format("Car booking failed on attempt %d", attemptCount); + logger.info("BookCarActivity failing: {}", errorMsg); + throw new RuntimeException(errorMsg); + } + + logger.info("BookCarActivity succeeding on attempt #{}", attemptCount); + return "Car booked successfully"; + } + } + + public static class CancelFlightActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelFlightActivity.class); + private static int attemptCount = 0; + + @Override + public Object run(WorkflowActivityContext ctx) { + attemptCount++; + logger.info("CancelFlightActivity attempt #{}", attemptCount); + return "Flight cancelled successfully"; + } + } + + public static class CancelHotelActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelHotelActivity.class); + private static int attemptCount = 0; + private static boolean shouldFail = false; + + @Override + public Object run(WorkflowActivityContext ctx) { + attemptCount++; + logger.info("CancelHotelActivity attempt #{} (shouldFail={})", attemptCount, shouldFail); + + if (shouldFail) { + String errorMsg = String.format("Hotel cancellation failed on attempt %d", attemptCount); + logger.info("CancelHotelActivity failing: {}", errorMsg); + throw new RuntimeException(errorMsg); + } + + logger.info("CancelHotelActivity succeeding on attempt #{}", attemptCount); + return "Hotel cancelled successfully"; + } + } + + public static class CancelCarActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(CancelCarActivity.class); + private static int attemptCount = 0; + + @Override + public Object run(WorkflowActivityContext ctx) { + attemptCount++; + logger.info("CancelCarActivity attempt #{}", attemptCount); + return "Car cancelled successfully"; + } + } + + @Test + public void testCompensationWithRetry() throws Exception { + // Set car booking to fail to trigger compensation + BookCarActivity.alwaysFail = true; + + // Create workflow input to make car booking fail + WorkflowInput input = new WorkflowInput(true, false); + System.out.println("Starting testCompensationWithRetry with input: " + input); + + // Start the workflow + String instanceId = workflowClient.scheduleNewWorkflow(BookTripWorkflow.class, input); + assertNotNull(instanceId, "Workflow instance ID should not be null"); + System.out.println("Started workflow with instance ID: " + instanceId); + + // Wait for workflow to start & complete + workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); + WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(120), true); + assertNotNull(status, "Workflow status should not be null"); + + // Verify the workflow completed with compensation + assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), + "Workflow should have completed with compensation"); + + String result = status.readOutputAs(String.class); + assertNotNull(result, "Workflow result should not be null"); + assertEquals("Workflow failed, compensation applied", result, + "Workflow should indicate compensation was applied"); + + // Verify compensations were executed (car booking failed so no car cancellation) + assertEquals(1, CancelFlightActivity.attemptCount, "Flight should be cancelled once"); + assertEquals(1, CancelHotelActivity.attemptCount, "Hotel should be cancelled once"); + assertEquals(0, CancelCarActivity.attemptCount, "Car should not be cancelled since booking failed"); + } + + @Test + public void testCompensationWithRetryFailure() throws Exception { + // Set car booking to fail to trigger compensation + BookCarActivity.alwaysFail = true; + // Set hotel cancellation to fail during compensation + CancelHotelActivity.shouldFail = true; + + // Create workflow input to make hotel cancellation fail + WorkflowInput input = new WorkflowInput(false, true); + System.out.println("Starting testCompensationWithRetryFailure with input: " + input); + + // Start the workflow + String instanceId = workflowClient.scheduleNewWorkflow(BookTripWorkflow.class, input); + assertNotNull(instanceId, "Workflow instance ID should not be null"); + System.out.println("Started workflow with instance ID: " + instanceId); + + // Wait for workflow to start & complete + workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); + WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(120), true); + assertNotNull(status, "Workflow status should not be null"); + assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), + "Workflow should have completed with compensation despite hotel cancellation failure"); + + String result = status.readOutputAs(String.class); + assertNotNull(result, "Workflow result should not be null"); + assertEquals("Workflow failed, compensation applied", result, + "Workflow should indicate compensation was applied"); + + // Verify all compensations were attempted + assertEquals(1, CancelFlightActivity.attemptCount, "Flight should be cancelled once"); + assertEquals(3, CancelHotelActivity.attemptCount, "Hotel cancellation should have retried twice before failing"); + assertEquals(0, CancelCarActivity.attemptCount, "Car should not be cancelled since booking failed"); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java new file mode 100644 index 0000000000..d060a646e7 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java @@ -0,0 +1,202 @@ +/* + * Copyright 2025 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.it.testcontainers; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import io.dapr.workflows.WorkflowStub; +import io.dapr.workflows.WorkflowTaskOptions; +import io.dapr.workflows.WorkflowTaskRetryPolicy; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.WorkflowInstanceStatus; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.workflows.client.WorkflowFailureDetails; +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; + +import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SpringBootTest( + webEnvironment = WebEnvironment.RANDOM_PORT, + classes = { + TestDaprWorkflowsConfiguration.class, + TestWorkflowsApplication.class + } +) +@Testcontainers +@Tag("testcontainers") +public class WorkflowRetryIT { + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("workflow-retry-app") + .withNetwork(DAPR_NETWORK) + .withComponent(new Component("kvstore", "state.in-memory", "v1", + Map.of("actorStateStore", "true"))) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withAppChannelAddress("host.testcontainers.internal"); + + /** + * Expose the Dapr ports to the host. + * + * @param registry the dynamic property registry + */ + @DynamicPropertySource + static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); + registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); + } + + @Autowired + private DaprWorkflowClient workflowClient; + + @Autowired + private WorkflowRuntimeBuilder workflowRuntimeBuilder; + + private WorkflowRuntime runtime; + + @BeforeEach + public void init() { + RetryTestActivity.attemptCount = 0; + RetryTestActivity.alwaysFail = false; + + runtime = workflowRuntimeBuilder.build(); + System.out.println("Start workflow runtime"); + runtime.start(false); + } + + public static class RetryTestWorkflowImpl implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting RetryTestWorkflowImpl"); + + WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() + .setMaxNumberOfAttempts(3) + .setFirstRetryInterval(Duration.ofSeconds(1)) + .setRetryTimeout(Duration.ofSeconds(30)) + .build(); + + WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy); + + try { + // Call the test activity with retry policy + String result = ctx.callActivity( + RetryTestActivity.class.getCanonicalName(), + null, + options, + String.class).await(); + + ctx.getLogger().info("Activity completed with result: {}", result); + ctx.complete(result); + } catch (Exception ex) { + ctx.getLogger().error("Workflow caught exception: {}", ex.getMessage()); + throw ex; + } + }; + } + } + + public static class RetryTestActivity implements WorkflowActivity { + private static final Logger logger = LoggerFactory.getLogger(RetryTestActivity.class); + private static int attemptCount = 0; + private static boolean alwaysFail = false; + + @Override + public Object run(WorkflowActivityContext ctx) { + attemptCount++; + logger.info("RetryTestActivity attempt #{}", attemptCount); + + if (alwaysFail || attemptCount < 3) { + String errorMsg = "Simulated failure on attempt " + attemptCount; + logger.info("RetryTestActivity failing on attempt #{}: {}", attemptCount, errorMsg); + throw new RuntimeException(errorMsg); + } + + String result = "Activity succeeded after " + attemptCount + " attempts"; + logger.info("RetryTestActivity succeeding on attempt #{} with result: {}", attemptCount, result); + return result; + } + } + + @Test + public void testWorkflowRetry() throws Exception { + // Start the workflow + String instanceId = workflowClient.scheduleNewWorkflow(RetryTestWorkflowImpl.class); + assertNotNull(instanceId, "Workflow instance ID should not be null"); + + // Wait for workflow to start & complete + workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); + WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true); + assertNotNull(status, "Workflow status should not be null"); + assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), + "Workflow should have completed successfully"); + + String result = status.readOutputAs(String.class); + assertNotNull(result, "Workflow result should not be null"); + assertEquals("Activity succeeded after 3 attempts", result, + "Activity should have succeeded after 3 attempts"); + } + + @Test + public void testWorkflowRetryWithFailure() throws Exception { + // Set activity to always fail + RetryTestActivity.alwaysFail = true; + + // Start the workflow + String instanceId = workflowClient.scheduleNewWorkflow(RetryTestWorkflowImpl.class); + assertNotNull(instanceId, "Workflow instance ID should not be null"); + + // Wait for workflow to start & complete + workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); + WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true); + assertNotNull(status, "Workflow status should not be null"); + + assertEquals(WorkflowRuntimeStatus.FAILED, status.getRuntimeStatus(), + "Workflow should have failed after retries"); + WorkflowFailureDetails failure = status.getFailureDetails(); + assertNotNull(failure, "Failure details should not be null"); + String errorMessage = failure.getErrorMessage(); + System.out.println("Error message: " + errorMessage); + assertTrue(errorMessage.contains("Simulated failure on attempt 3"), + "Error should indicate failure on final attempt. Actual error: " + errorMessage); + } +} From 8a27e7953b3c27e41dfe0232944f43e89978558e Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 29 May 2025 09:59:28 -0500 Subject: [PATCH 17/22] add test for no compensation if successful and assert attempts Signed-off-by: Cassandra Coyle --- .../WorkflowRetryCompensationIT.java | 34 +++++++++++++++++++ .../it/testcontainers/WorkflowRetryIT.java | 4 +++ 2 files changed, 38 insertions(+) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java index 9807c4d1cb..75c42c0804 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java @@ -419,4 +419,38 @@ public void testCompensationWithRetryFailure() throws Exception { assertEquals(3, CancelHotelActivity.attemptCount, "Hotel cancellation should have retried twice before failing"); assertEquals(0, CancelCarActivity.attemptCount, "Car should not be cancelled since booking failed"); } + + @Test + public void testRetrySuccessNoCompensation() throws Exception { + // Let car booking retry and succeed (default behavior) + BookCarActivity.alwaysFail = false; + + WorkflowInput input = new WorkflowInput(false, false); + System.out.println("Starting testRetrySuccessNoCompensation with input: " + input); + + // Start the workflow + String instanceId = workflowClient.scheduleNewWorkflow(BookTripWorkflow.class, input); + assertNotNull(instanceId, "Workflow instance ID should not be null"); + System.out.println("Started workflow with instance ID: " + instanceId); + + // Wait for workflow to start & complete + workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); + WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(120), true); + assertNotNull(status, "Workflow status should not be null"); + assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), + "Workflow should have completed successfully"); + + String result = status.readOutputAs(String.class); + assertNotNull(result, "Workflow result should not be null"); + assertEquals("Flight booked successfully, Hotel booked successfully, Car booked successfully", result, + "All bookings should have succeeded"); + + // Assert all booking attempts & no compensations ran + assertEquals(1, BookFlightActivity.attemptCount, "Flight should succeed on first attempt"); + assertEquals(1, BookHotelActivity.attemptCount, "Hotel should succeed on first attempt"); + assertEquals(3, BookCarActivity.attemptCount, "Car should succeed on 3rd attempt after 2 retries"); + assertEquals(0, CancelFlightActivity.attemptCount, "No flight cancellation should occur"); + assertEquals(0, CancelHotelActivity.attemptCount, "No hotel cancellation should occur"); + assertEquals(0, CancelCarActivity.attemptCount, "No car cancellation should occur"); + } } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java index d060a646e7..8f21814757 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java @@ -174,6 +174,8 @@ public void testWorkflowRetry() throws Exception { assertNotNull(result, "Workflow result should not be null"); assertEquals("Activity succeeded after 3 attempts", result, "Activity should have succeeded after 3 attempts"); + + assertEquals(3, RetryTestActivity.attemptCount, "Activity should have been attempted 3 times"); } @Test @@ -198,5 +200,7 @@ public void testWorkflowRetryWithFailure() throws Exception { System.out.println("Error message: " + errorMessage); assertTrue(errorMessage.contains("Simulated failure on attempt 3"), "Error should indicate failure on final attempt. Actual error: " + errorMessage); + + assertEquals(3, RetryTestActivity.attemptCount, "Activity should have failed after 3 attempts"); } } From 8fc18f9f2a222d25574412b67e9fd5a1ee490d2d Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 29 May 2025 10:10:16 -0500 Subject: [PATCH 18/22] update mechanical markdown Signed-off-by: Cassandra Coyle --- examples/src/main/java/io/dapr/examples/workflows/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 4fdec23657..4d9c057f24 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -621,7 +621,6 @@ expected_stdout_lines: - "Forcing Failure to trigger compensation for activity: io.dapr.examples.workflows.compensation.BookCarActivity" - "******** executing compensation logic ********" - "Activity failed: Task 'io.dapr.examples.workflows.compensation.BookCarActivity' (#2) failed with an unhandled exception: Failed to book car" - - "Error during compensation: The orchestrator is blocked and waiting for new inputs. This Throwable should never be caught by user code." - "Starting Activity: io.dapr.examples.workflows.compensation.CancelHotelActivity" - "Activity completed with result: Hotel canceled successfully" - "Starting Activity: io.dapr.examples.workflows.compensation.CancelFlightActivity" From 2528a2161297f448ac199eb5fc705fa7ed5dee87 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 29 May 2025 11:34:12 -0500 Subject: [PATCH 19/22] add back pubsub... but this should be removed long term Signed-off-by: Cassandra Coyle --- .../src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java | 1 + .../io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java | 1 + .../src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java | 1 + 3 files changed, 3 insertions(+) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java index 66f86a2c2b..5c6a360c8a 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java @@ -62,6 +62,7 @@ public class DaprWorkflowsIT { .withNetwork(DAPR_NETWORK) .withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withDaprLogLevel(DaprLogLevel.DEBUG) .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .withAppChannelAddress("host.testcontainers.internal"); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java index 75c42c0804..10541d01f1 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java @@ -74,6 +74,7 @@ public class WorkflowRetryCompensationIT { .withNetwork(DAPR_NETWORK) .withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withDaprLogLevel(DaprLogLevel.DEBUG) .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .withAppChannelAddress("host.testcontainers.internal"); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java index 8f21814757..ba3bbcee9b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java @@ -70,6 +70,7 @@ public class WorkflowRetryIT { .withNetwork(DAPR_NETWORK) .withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withDaprLogLevel(DaprLogLevel.DEBUG) .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .withAppChannelAddress("host.testcontainers.internal"); From da765134570e546f443c78648eb143eb7c3ac469 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 29 May 2025 12:58:52 -0500 Subject: [PATCH 20/22] try adding waitforsidecar Signed-off-by: Cassandra Coyle --- .../it/testcontainers/DaprWorkflowsIT.java | 23 ++++++++++++++++++- .../WorkflowRetryCompensationIT.java | 23 ++++++++++++++++++- .../it/testcontainers/WorkflowRetryIT.java | 23 ++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java index 5c6a360c8a..d37b8575f6 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java @@ -15,6 +15,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.config.Properties; +import io.dapr.config.Property; import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; @@ -90,7 +94,24 @@ static void daprProperties(DynamicPropertyRegistry registry) { * Initializes the test. */ @BeforeEach - public void init() { + public void init() throws InterruptedException { + // Wait for Dapr sidecar to be ready before starting workflow runtime + Map, String> overrides = Map.of( + Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint(), + Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint() + ); + + while (true) { + try (DaprClient client = new DaprClientBuilder() + .withPropertyOverrides(overrides).build()) { + client.waitForSidecar(10000).block(); // 10 seconds + break; + } catch (Exception e) { + System.out.println("Sidecar not ready yet, retrying in 10 seconds..."); + Thread.sleep(1000); + } + } + WorkflowRuntime runtime = workflowRuntimeBuilder.build(); System.out.println("Start workflow runtime"); runtime.start(false); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java index 10541d01f1..bb5263ba3b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java @@ -28,6 +28,10 @@ import io.dapr.workflows.client.WorkflowFailureDetails; import io.dapr.workflows.runtime.WorkflowRuntime; import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.config.Properties; +import io.dapr.config.Property; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -94,7 +98,7 @@ static void daprProperties(DynamicPropertyRegistry registry) { private WorkflowRuntime runtime; @BeforeEach - public void init() { + public void init() throws InterruptedException { // Reset attempt counts before each test BookFlightActivity.attemptCount = 0; BookHotelActivity.attemptCount = 0; @@ -107,6 +111,23 @@ public void init() { BookCarActivity.alwaysFail = false; CancelHotelActivity.shouldFail = false; + // Wait for Dapr sidecar to be ready before starting workflow runtime + Map, String> overrides = Map.of( + Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint(), + Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint() + ); + + while (true) { + try (DaprClient client = new DaprClientBuilder() + .withPropertyOverrides(overrides).build()) { + client.waitForSidecar(10000).block(); // 10 seconds + break; + } catch (Exception e) { + System.out.println("Sidecar not ready yet, retrying in 10 seconds..."); + Thread.sleep(1000); + } + } + runtime = workflowRuntimeBuilder.build(); System.out.println("Starting new workflow runtime for test"); runtime.start(false); diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java index ba3bbcee9b..5cfce75d67 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java @@ -28,6 +28,10 @@ import io.dapr.workflows.client.WorkflowFailureDetails; import io.dapr.workflows.runtime.WorkflowRuntime; import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.config.Properties; +import io.dapr.config.Property; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -95,10 +99,27 @@ static void daprProperties(DynamicPropertyRegistry registry) { private WorkflowRuntime runtime; @BeforeEach - public void init() { + public void init() throws InterruptedException { RetryTestActivity.attemptCount = 0; RetryTestActivity.alwaysFail = false; + // Wait for Dapr sidecar to be ready before starting workflow runtime + Map, String> overrides = Map.of( + Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint(), + Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint() + ); + + while (true) { + try (DaprClient client = new DaprClientBuilder() + .withPropertyOverrides(overrides).build()) { + client.waitForSidecar(10000).block(); // 10 seconds + break; + } catch (Exception e) { + System.out.println("Sidecar not ready yet, retrying in 10 seconds..."); + Thread.sleep(1000); + } + } + runtime = workflowRuntimeBuilder.build(); System.out.println("Start workflow runtime"); runtime.start(false); From e9455acc90ef47f36671b8741e93374a9136c41d Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 29 May 2025 13:35:56 -0500 Subject: [PATCH 21/22] rm tests from examples pr Signed-off-by: Cassandra Coyle --- .../it/testcontainers/DaprWorkflowsIT.java | 48 +- .../TestDaprWorkflowsConfiguration.java | 16 +- .../WorkflowRetryCompensationIT.java | 478 ------------------ .../it/testcontainers/WorkflowRetryIT.java | 228 --------- 4 files changed, 30 insertions(+), 740 deletions(-) delete mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java delete mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java index d37b8575f6..bb1f1c7689 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java @@ -15,10 +15,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.config.Properties; -import io.dapr.config.Property; + import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; @@ -44,6 +41,7 @@ import java.util.Map; import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -94,24 +92,7 @@ static void daprProperties(DynamicPropertyRegistry registry) { * Initializes the test. */ @BeforeEach - public void init() throws InterruptedException { - // Wait for Dapr sidecar to be ready before starting workflow runtime - Map, String> overrides = Map.of( - Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint(), - Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint() - ); - - while (true) { - try (DaprClient client = new DaprClientBuilder() - .withPropertyOverrides(overrides).build()) { - client.waitForSidecar(10000).block(); // 10 seconds - break; - } catch (Exception e) { - System.out.println("Sidecar not ready yet, retrying in 10 seconds..."); - Thread.sleep(1000); - } - } - + public void init() { WorkflowRuntime runtime = workflowRuntimeBuilder.build(); System.out.println("Start workflow runtime"); runtime.start(false); @@ -138,6 +119,29 @@ public void testWorkflows() throws Exception { assertEquals(instanceId, workflowOutput.getWorkflowId()); } + @Test + public void testExecutionKeyWorkflows() throws Exception { + TestWorkflowPayload payload = new TestWorkflowPayload(new ArrayList<>()); + String instanceId = workflowClient.scheduleNewWorkflow(TestExecutionKeysWorkflow.class, payload); + + workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(100), false); + + Duration timeout = Duration.ofSeconds(1000); + WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, timeout, true); + + assertNotNull(workflowStatus); + + TestWorkflowPayload workflowOutput = deserialize(workflowStatus.getSerializedOutput()); + + assertEquals(1, workflowOutput.getPayloads().size()); + assertEquals("Execution key found", workflowOutput.getPayloads().get(0)); + + String executionKey = workflowOutput.getWorkflowId() +"-"+"io.dapr.it.testcontainers.TaskExecutionKeyActivity"; + assertTrue(KeyStore.getInstance().getKey(executionKey)); + + assertEquals(instanceId, workflowOutput.getWorkflowId()); + } + private TestWorkflowPayload deserialize(String value) throws JsonProcessingException { return OBJECT_MAPPER.readValue(value, TestWorkflowPayload.class); } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java index c1417ebbc5..e868b18870 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java @@ -56,20 +56,12 @@ public WorkflowRuntimeBuilder workflowRuntimeBuilder( WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(new Properties(overrides)); builder.registerWorkflow(TestWorkflow.class); + builder.registerWorkflow(TestExecutionKeysWorkflow.class); builder.registerActivity(FirstActivity.class); builder.registerActivity(SecondActivity.class); - - builder.registerWorkflow(WorkflowRetryIT.RetryTestWorkflowImpl.class); - builder.registerActivity(WorkflowRetryIT.RetryTestActivity.class); - - builder.registerWorkflow(WorkflowRetryCompensationIT.BookTripWorkflow.class); - builder.registerActivity(WorkflowRetryCompensationIT.BookFlightActivity.class); - builder.registerActivity(WorkflowRetryCompensationIT.BookHotelActivity.class); - builder.registerActivity(WorkflowRetryCompensationIT.BookCarActivity.class); - builder.registerActivity(WorkflowRetryCompensationIT.CancelFlightActivity.class); - builder.registerActivity(WorkflowRetryCompensationIT.CancelHotelActivity.class); - builder.registerActivity(WorkflowRetryCompensationIT.CancelCarActivity.class); - + builder.registerActivity(TaskExecutionKeyActivity.class); + + return builder; } } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java deleted file mode 100644 index bb5263ba3b..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryCompensationIT.java +++ /dev/null @@ -1,478 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.testcontainers; - -import io.dapr.testcontainers.Component; -import io.dapr.testcontainers.DaprContainer; -import io.dapr.testcontainers.DaprLogLevel; -import io.dapr.workflows.Workflow; -import io.dapr.workflows.WorkflowActivity; -import io.dapr.workflows.WorkflowActivityContext; -import io.dapr.workflows.WorkflowStub; -import io.dapr.workflows.WorkflowTaskOptions; -import io.dapr.workflows.WorkflowTaskRetryPolicy; -import io.dapr.workflows.client.DaprWorkflowClient; -import io.dapr.workflows.client.WorkflowInstanceStatus; -import io.dapr.workflows.client.WorkflowRuntimeStatus; -import io.dapr.workflows.client.WorkflowFailureDetails; -import io.dapr.workflows.runtime.WorkflowRuntime; -import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; -import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.config.Properties; -import io.dapr.config.Property; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.Network; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.dapr.durabletask.TaskFailedException; - -@SpringBootTest( - webEnvironment = WebEnvironment.RANDOM_PORT, - classes = { - TestDaprWorkflowsConfiguration.class, - TestWorkflowsApplication.class - } -) -@Testcontainers -@Tag("testcontainers") -public class WorkflowRetryCompensationIT { - - private static final Network DAPR_NETWORK = Network.newNetwork(); - - @Container - private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) - .withAppName("workflow-retry-compensation-app") - .withNetwork(DAPR_NETWORK) - .withComponent(new Component("kvstore", "state.in-memory", "v1", - Map.of("actorStateStore", "true"))) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) - .withDaprLogLevel(DaprLogLevel.DEBUG) - .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) - .withAppChannelAddress("host.testcontainers.internal"); - - @DynamicPropertySource - static void daprProperties(DynamicPropertyRegistry registry) { - registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); - registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); - } - - @Autowired - private DaprWorkflowClient workflowClient; - - @Autowired - private WorkflowRuntimeBuilder workflowRuntimeBuilder; - - private WorkflowRuntime runtime; - - @BeforeEach - public void init() throws InterruptedException { - // Reset attempt counts before each test - BookFlightActivity.attemptCount = 0; - BookHotelActivity.attemptCount = 0; - BookCarActivity.attemptCount = 0; - CancelFlightActivity.attemptCount = 0; - CancelHotelActivity.attemptCount = 0; - CancelCarActivity.attemptCount = 0; - - // Reset failure flags - BookCarActivity.alwaysFail = false; - CancelHotelActivity.shouldFail = false; - - // Wait for Dapr sidecar to be ready before starting workflow runtime - Map, String> overrides = Map.of( - Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint(), - Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint() - ); - - while (true) { - try (DaprClient client = new DaprClientBuilder() - .withPropertyOverrides(overrides).build()) { - client.waitForSidecar(10000).block(); // 10 seconds - break; - } catch (Exception e) { - System.out.println("Sidecar not ready yet, retrying in 10 seconds..."); - Thread.sleep(1000); - } - } - - runtime = workflowRuntimeBuilder.build(); - System.out.println("Starting new workflow runtime for test"); - runtime.start(false); - } - - public static class WorkflowInput { - private boolean failCarBooking; - private boolean failHotelCancellation; - - // Default constructor - public WorkflowInput() { - this.failCarBooking = false; - this.failHotelCancellation = false; - } - - public WorkflowInput(boolean failCarBooking, boolean failHotelCancellation) { - this.failCarBooking = failCarBooking; - this.failHotelCancellation = failHotelCancellation; - } - - public boolean isFailCarBooking() { - return failCarBooking; - } - - public void setFailCarBooking(boolean failCarBooking) { - this.failCarBooking = failCarBooking; - } - - public boolean isFailHotelCancellation() { - return failHotelCancellation; - } - - public void setFailHotelCancellation(boolean failHotelCancellation) { - this.failHotelCancellation = failHotelCancellation; - } - } - - public static class BookTripWorkflow implements Workflow { - @Override - public WorkflowStub create() { - return ctx -> { - ctx.getLogger().info("Starting BookTripWorkflow"); - List compensations = new ArrayList<>(); - - // Get workflow input - WorkflowInput input = ctx.getInput(WorkflowInput.class); - ctx.getLogger().info("Workflow input: failCarBooking={}, failHotelCancellation={}", - input.isFailCarBooking(), input.isFailHotelCancellation()); - - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(3) - .setFirstRetryInterval(Duration.ofSeconds(1)) - .setRetryTimeout(Duration.ofSeconds(30)) - .build(); - - WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy); - - try { - // Book flight (should succeed) - ctx.getLogger().info("Attempting to book flight..."); - String flightResult = ctx.callActivity( - BookFlightActivity.class.getCanonicalName(), - null, - options, - String.class).await(); - ctx.getLogger().info("Flight booking completed: {}", flightResult); - compensations.add("CancelFlight"); - ctx.getLogger().info("Added flight cancellation to compensation list. Current compensations: {}", compensations); - - // Book hotel (should succeed) - ctx.getLogger().info("Attempting to book hotel..."); - String hotelResult = ctx.callActivity( - BookHotelActivity.class.getCanonicalName(), - null, - options, - String.class).await(); - ctx.getLogger().info("Hotel booking completed: {}", hotelResult); - compensations.add("CancelHotel"); - ctx.getLogger().info("Added hotel cancellation to compensation list. Current compensations: {}", compensations); - - // Book car (should fail if configured) - ctx.getLogger().info("Attempting to book car with shouldFail={}...", input.isFailCarBooking()); - String carResult = ctx.callActivity( - BookCarActivity.class.getCanonicalName(), - null, - options, - String.class).await(); - ctx.getLogger().info("Car booking completed: {}", carResult); - compensations.add("CancelCar"); - ctx.getLogger().info("Added car cancellation to compensation list. Current compensations: {}", compensations); - - String result = String.format("%s, %s, %s", flightResult, hotelResult, carResult); - ctx.getLogger().info("Trip booked successfully: {}", result); - ctx.complete(result); - - } catch (TaskFailedException e) { - - ctx.getLogger().error("Activity failed: {}", e.getMessage()); - ctx.getLogger().info("******** executing compensation logic ********"); - ctx.getLogger().info("Compensation list before reversal: {}", compensations); - - // Execute compensations in reverse order - Collections.reverse(compensations); - ctx.getLogger().info("Compensation list after reversal: {}", compensations); - - for (String compensation : compensations) { - try { - ctx.getLogger().info("Executing compensation: {}", compensation); - switch (compensation) { - case "CancelCar": - ctx.getLogger().info("Calling CancelCarActivity..."); - String carCancelResult = ctx.callActivity( - CancelCarActivity.class.getCanonicalName(), - null, - options, - String.class).await(); - ctx.getLogger().info("Car cancellation completed: {}", carCancelResult); - break; - - case "CancelHotel": - ctx.getLogger().info("Calling CancelHotelActivity with shouldFail={}...", input.isFailHotelCancellation()); - String hotelCancelResult = ctx.callActivity( - CancelHotelActivity.class.getCanonicalName(), - null, // No input needed, use static flag - options, - String.class).await(); - ctx.getLogger().info("Hotel cancellation completed: {}", hotelCancelResult); - break; - - case "CancelFlight": - ctx.getLogger().info("Calling CancelFlightActivity..."); - String flightCancelResult = ctx.callActivity( - CancelFlightActivity.class.getCanonicalName(), - null, - options, - String.class).await(); - ctx.getLogger().info("Flight cancellation completed: {}", flightCancelResult); - break; - } - } catch (TaskFailedException ex) { - ctx.getLogger().error("Compensation activity {} failed: {}", compensation, ex.getMessage()); - } - } - ctx.getLogger().info("All compensations executed. Completing workflow."); - ctx.complete("Workflow failed, compensation applied"); - } - }; - } - } - - public static class BookFlightActivity implements WorkflowActivity { - private static final Logger logger = LoggerFactory.getLogger(BookFlightActivity.class); - private static int attemptCount = 0; - - @Override - public Object run(WorkflowActivityContext ctx) { - attemptCount++; - logger.info("BookFlightActivity attempt #{}", attemptCount); - return "Flight booked successfully"; - } - } - - public static class BookHotelActivity implements WorkflowActivity { - private static final Logger logger = LoggerFactory.getLogger(BookHotelActivity.class); - private static int attemptCount = 0; - - @Override - public Object run(WorkflowActivityContext ctx) { - attemptCount++; - logger.info("BookHotelActivity attempt #{}", attemptCount); - return "Hotel booked successfully"; - } - } - - public static class BookCarActivity implements WorkflowActivity { - private static final Logger logger = LoggerFactory.getLogger(BookCarActivity.class); - private static int attemptCount = 0; - private static boolean alwaysFail = false; - - @Override - public Object run(WorkflowActivityContext ctx) { - attemptCount++; - logger.info("BookCarActivity attempt #{} (alwaysFail={})", attemptCount, alwaysFail); - - if (alwaysFail) { - String errorMsg = String.format("Car booking failed on attempt %d (alwaysFail=true)", attemptCount); - logger.info("BookCarActivity failing: {}", errorMsg); - throw new RuntimeException(errorMsg); - } - - if (attemptCount < 3) { - String errorMsg = String.format("Car booking failed on attempt %d", attemptCount); - logger.info("BookCarActivity failing: {}", errorMsg); - throw new RuntimeException(errorMsg); - } - - logger.info("BookCarActivity succeeding on attempt #{}", attemptCount); - return "Car booked successfully"; - } - } - - public static class CancelFlightActivity implements WorkflowActivity { - private static final Logger logger = LoggerFactory.getLogger(CancelFlightActivity.class); - private static int attemptCount = 0; - - @Override - public Object run(WorkflowActivityContext ctx) { - attemptCount++; - logger.info("CancelFlightActivity attempt #{}", attemptCount); - return "Flight cancelled successfully"; - } - } - - public static class CancelHotelActivity implements WorkflowActivity { - private static final Logger logger = LoggerFactory.getLogger(CancelHotelActivity.class); - private static int attemptCount = 0; - private static boolean shouldFail = false; - - @Override - public Object run(WorkflowActivityContext ctx) { - attemptCount++; - logger.info("CancelHotelActivity attempt #{} (shouldFail={})", attemptCount, shouldFail); - - if (shouldFail) { - String errorMsg = String.format("Hotel cancellation failed on attempt %d", attemptCount); - logger.info("CancelHotelActivity failing: {}", errorMsg); - throw new RuntimeException(errorMsg); - } - - logger.info("CancelHotelActivity succeeding on attempt #{}", attemptCount); - return "Hotel cancelled successfully"; - } - } - - public static class CancelCarActivity implements WorkflowActivity { - private static final Logger logger = LoggerFactory.getLogger(CancelCarActivity.class); - private static int attemptCount = 0; - - @Override - public Object run(WorkflowActivityContext ctx) { - attemptCount++; - logger.info("CancelCarActivity attempt #{}", attemptCount); - return "Car cancelled successfully"; - } - } - - @Test - public void testCompensationWithRetry() throws Exception { - // Set car booking to fail to trigger compensation - BookCarActivity.alwaysFail = true; - - // Create workflow input to make car booking fail - WorkflowInput input = new WorkflowInput(true, false); - System.out.println("Starting testCompensationWithRetry with input: " + input); - - // Start the workflow - String instanceId = workflowClient.scheduleNewWorkflow(BookTripWorkflow.class, input); - assertNotNull(instanceId, "Workflow instance ID should not be null"); - System.out.println("Started workflow with instance ID: " + instanceId); - - // Wait for workflow to start & complete - workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); - WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(120), true); - assertNotNull(status, "Workflow status should not be null"); - - // Verify the workflow completed with compensation - assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), - "Workflow should have completed with compensation"); - - String result = status.readOutputAs(String.class); - assertNotNull(result, "Workflow result should not be null"); - assertEquals("Workflow failed, compensation applied", result, - "Workflow should indicate compensation was applied"); - - // Verify compensations were executed (car booking failed so no car cancellation) - assertEquals(1, CancelFlightActivity.attemptCount, "Flight should be cancelled once"); - assertEquals(1, CancelHotelActivity.attemptCount, "Hotel should be cancelled once"); - assertEquals(0, CancelCarActivity.attemptCount, "Car should not be cancelled since booking failed"); - } - - @Test - public void testCompensationWithRetryFailure() throws Exception { - // Set car booking to fail to trigger compensation - BookCarActivity.alwaysFail = true; - // Set hotel cancellation to fail during compensation - CancelHotelActivity.shouldFail = true; - - // Create workflow input to make hotel cancellation fail - WorkflowInput input = new WorkflowInput(false, true); - System.out.println("Starting testCompensationWithRetryFailure with input: " + input); - - // Start the workflow - String instanceId = workflowClient.scheduleNewWorkflow(BookTripWorkflow.class, input); - assertNotNull(instanceId, "Workflow instance ID should not be null"); - System.out.println("Started workflow with instance ID: " + instanceId); - - // Wait for workflow to start & complete - workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); - WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(120), true); - assertNotNull(status, "Workflow status should not be null"); - assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), - "Workflow should have completed with compensation despite hotel cancellation failure"); - - String result = status.readOutputAs(String.class); - assertNotNull(result, "Workflow result should not be null"); - assertEquals("Workflow failed, compensation applied", result, - "Workflow should indicate compensation was applied"); - - // Verify all compensations were attempted - assertEquals(1, CancelFlightActivity.attemptCount, "Flight should be cancelled once"); - assertEquals(3, CancelHotelActivity.attemptCount, "Hotel cancellation should have retried twice before failing"); - assertEquals(0, CancelCarActivity.attemptCount, "Car should not be cancelled since booking failed"); - } - - @Test - public void testRetrySuccessNoCompensation() throws Exception { - // Let car booking retry and succeed (default behavior) - BookCarActivity.alwaysFail = false; - - WorkflowInput input = new WorkflowInput(false, false); - System.out.println("Starting testRetrySuccessNoCompensation with input: " + input); - - // Start the workflow - String instanceId = workflowClient.scheduleNewWorkflow(BookTripWorkflow.class, input); - assertNotNull(instanceId, "Workflow instance ID should not be null"); - System.out.println("Started workflow with instance ID: " + instanceId); - - // Wait for workflow to start & complete - workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); - WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(120), true); - assertNotNull(status, "Workflow status should not be null"); - assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), - "Workflow should have completed successfully"); - - String result = status.readOutputAs(String.class); - assertNotNull(result, "Workflow result should not be null"); - assertEquals("Flight booked successfully, Hotel booked successfully, Car booked successfully", result, - "All bookings should have succeeded"); - - // Assert all booking attempts & no compensations ran - assertEquals(1, BookFlightActivity.attemptCount, "Flight should succeed on first attempt"); - assertEquals(1, BookHotelActivity.attemptCount, "Hotel should succeed on first attempt"); - assertEquals(3, BookCarActivity.attemptCount, "Car should succeed on 3rd attempt after 2 retries"); - assertEquals(0, CancelFlightActivity.attemptCount, "No flight cancellation should occur"); - assertEquals(0, CancelHotelActivity.attemptCount, "No hotel cancellation should occur"); - assertEquals(0, CancelCarActivity.attemptCount, "No car cancellation should occur"); - } -} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java deleted file mode 100644 index 5cfce75d67..0000000000 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/WorkflowRetryIT.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * Copyright 2025 The Dapr Authors - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and -limitations under the License. -*/ - -package io.dapr.it.testcontainers; - -import io.dapr.testcontainers.Component; -import io.dapr.testcontainers.DaprContainer; -import io.dapr.testcontainers.DaprLogLevel; -import io.dapr.workflows.Workflow; -import io.dapr.workflows.WorkflowActivity; -import io.dapr.workflows.WorkflowActivityContext; -import io.dapr.workflows.WorkflowStub; -import io.dapr.workflows.WorkflowTaskOptions; -import io.dapr.workflows.WorkflowTaskRetryPolicy; -import io.dapr.workflows.client.DaprWorkflowClient; -import io.dapr.workflows.client.WorkflowInstanceStatus; -import io.dapr.workflows.client.WorkflowRuntimeStatus; -import io.dapr.workflows.client.WorkflowFailureDetails; -import io.dapr.workflows.runtime.WorkflowRuntime; -import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; -import io.dapr.client.DaprClient; -import io.dapr.client.DaprClientBuilder; -import io.dapr.config.Properties; -import io.dapr.config.Property; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.Network; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.time.Duration; -import java.util.Collections; -import java.util.Map; - -import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -@SpringBootTest( - webEnvironment = WebEnvironment.RANDOM_PORT, - classes = { - TestDaprWorkflowsConfiguration.class, - TestWorkflowsApplication.class - } -) -@Testcontainers -@Tag("testcontainers") -public class WorkflowRetryIT { - - private static final Network DAPR_NETWORK = Network.newNetwork(); - - @Container - private static final DaprContainer DAPR_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) - .withAppName("workflow-retry-app") - .withNetwork(DAPR_NETWORK) - .withComponent(new Component("kvstore", "state.in-memory", "v1", - Map.of("actorStateStore", "true"))) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) - .withDaprLogLevel(DaprLogLevel.DEBUG) - .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) - .withAppChannelAddress("host.testcontainers.internal"); - - /** - * Expose the Dapr ports to the host. - * - * @param registry the dynamic property registry - */ - @DynamicPropertySource - static void daprProperties(DynamicPropertyRegistry registry) { - registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); - registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); - } - - @Autowired - private DaprWorkflowClient workflowClient; - - @Autowired - private WorkflowRuntimeBuilder workflowRuntimeBuilder; - - private WorkflowRuntime runtime; - - @BeforeEach - public void init() throws InterruptedException { - RetryTestActivity.attemptCount = 0; - RetryTestActivity.alwaysFail = false; - - // Wait for Dapr sidecar to be ready before starting workflow runtime - Map, String> overrides = Map.of( - Properties.HTTP_ENDPOINT, DAPR_CONTAINER.getHttpEndpoint(), - Properties.GRPC_ENDPOINT, DAPR_CONTAINER.getGrpcEndpoint() - ); - - while (true) { - try (DaprClient client = new DaprClientBuilder() - .withPropertyOverrides(overrides).build()) { - client.waitForSidecar(10000).block(); // 10 seconds - break; - } catch (Exception e) { - System.out.println("Sidecar not ready yet, retrying in 10 seconds..."); - Thread.sleep(1000); - } - } - - runtime = workflowRuntimeBuilder.build(); - System.out.println("Start workflow runtime"); - runtime.start(false); - } - - public static class RetryTestWorkflowImpl implements Workflow { - @Override - public WorkflowStub create() { - return ctx -> { - ctx.getLogger().info("Starting RetryTestWorkflowImpl"); - - WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder() - .setMaxNumberOfAttempts(3) - .setFirstRetryInterval(Duration.ofSeconds(1)) - .setRetryTimeout(Duration.ofSeconds(30)) - .build(); - - WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy); - - try { - // Call the test activity with retry policy - String result = ctx.callActivity( - RetryTestActivity.class.getCanonicalName(), - null, - options, - String.class).await(); - - ctx.getLogger().info("Activity completed with result: {}", result); - ctx.complete(result); - } catch (Exception ex) { - ctx.getLogger().error("Workflow caught exception: {}", ex.getMessage()); - throw ex; - } - }; - } - } - - public static class RetryTestActivity implements WorkflowActivity { - private static final Logger logger = LoggerFactory.getLogger(RetryTestActivity.class); - private static int attemptCount = 0; - private static boolean alwaysFail = false; - - @Override - public Object run(WorkflowActivityContext ctx) { - attemptCount++; - logger.info("RetryTestActivity attempt #{}", attemptCount); - - if (alwaysFail || attemptCount < 3) { - String errorMsg = "Simulated failure on attempt " + attemptCount; - logger.info("RetryTestActivity failing on attempt #{}: {}", attemptCount, errorMsg); - throw new RuntimeException(errorMsg); - } - - String result = "Activity succeeded after " + attemptCount + " attempts"; - logger.info("RetryTestActivity succeeding on attempt #{} with result: {}", attemptCount, result); - return result; - } - } - - @Test - public void testWorkflowRetry() throws Exception { - // Start the workflow - String instanceId = workflowClient.scheduleNewWorkflow(RetryTestWorkflowImpl.class); - assertNotNull(instanceId, "Workflow instance ID should not be null"); - - // Wait for workflow to start & complete - workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); - WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true); - assertNotNull(status, "Workflow status should not be null"); - assertEquals(WorkflowRuntimeStatus.COMPLETED, status.getRuntimeStatus(), - "Workflow should have completed successfully"); - - String result = status.readOutputAs(String.class); - assertNotNull(result, "Workflow result should not be null"); - assertEquals("Activity succeeded after 3 attempts", result, - "Activity should have succeeded after 3 attempts"); - - assertEquals(3, RetryTestActivity.attemptCount, "Activity should have been attempted 3 times"); - } - - @Test - public void testWorkflowRetryWithFailure() throws Exception { - // Set activity to always fail - RetryTestActivity.alwaysFail = true; - - // Start the workflow - String instanceId = workflowClient.scheduleNewWorkflow(RetryTestWorkflowImpl.class); - assertNotNull(instanceId, "Workflow instance ID should not be null"); - - // Wait for workflow to start & complete - workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); - WorkflowInstanceStatus status = workflowClient.waitForInstanceCompletion(instanceId, Duration.ofSeconds(60), true); - assertNotNull(status, "Workflow status should not be null"); - - assertEquals(WorkflowRuntimeStatus.FAILED, status.getRuntimeStatus(), - "Workflow should have failed after retries"); - WorkflowFailureDetails failure = status.getFailureDetails(); - assertNotNull(failure, "Failure details should not be null"); - String errorMessage = failure.getErrorMessage(); - System.out.println("Error message: " + errorMessage); - assertTrue(errorMessage.contains("Simulated failure on attempt 3"), - "Error should indicate failure on final attempt. Actual error: " + errorMessage); - - assertEquals(3, RetryTestActivity.attemptCount, "Activity should have failed after 3 attempts"); - } -} From 13a0891add1251ef2a858e0200c29e1cfa72c34a Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 29 May 2025 13:36:57 -0500 Subject: [PATCH 22/22] reset unintended changes Signed-off-by: Cassandra Coyle --- .../it/testcontainers/DaprWorkflowsIT.java | 25 ------------------- .../TestDaprWorkflowsConfiguration.java | 5 +--- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java index bb1f1c7689..5c6a360c8a 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsIT.java @@ -15,7 +15,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; - import io.dapr.testcontainers.Component; import io.dapr.testcontainers.DaprContainer; import io.dapr.testcontainers.DaprLogLevel; @@ -41,7 +40,6 @@ import java.util.Map; import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; -import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -119,29 +117,6 @@ public void testWorkflows() throws Exception { assertEquals(instanceId, workflowOutput.getWorkflowId()); } - @Test - public void testExecutionKeyWorkflows() throws Exception { - TestWorkflowPayload payload = new TestWorkflowPayload(new ArrayList<>()); - String instanceId = workflowClient.scheduleNewWorkflow(TestExecutionKeysWorkflow.class, payload); - - workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(100), false); - - Duration timeout = Duration.ofSeconds(1000); - WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, timeout, true); - - assertNotNull(workflowStatus); - - TestWorkflowPayload workflowOutput = deserialize(workflowStatus.getSerializedOutput()); - - assertEquals(1, workflowOutput.getPayloads().size()); - assertEquals("Execution key found", workflowOutput.getPayloads().get(0)); - - String executionKey = workflowOutput.getWorkflowId() +"-"+"io.dapr.it.testcontainers.TaskExecutionKeyActivity"; - assertTrue(KeyStore.getInstance().getKey(executionKey)); - - assertEquals(instanceId, workflowOutput.getWorkflowId()); - } - private TestWorkflowPayload deserialize(String value) throws JsonProcessingException { return OBJECT_MAPPER.readValue(value, TestWorkflowPayload.class); } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java index e868b18870..0a2487b70c 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestDaprWorkflowsConfiguration.java @@ -56,12 +56,9 @@ public WorkflowRuntimeBuilder workflowRuntimeBuilder( WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(new Properties(overrides)); builder.registerWorkflow(TestWorkflow.class); - builder.registerWorkflow(TestExecutionKeysWorkflow.class); builder.registerActivity(FirstActivity.class); builder.registerActivity(SecondActivity.class); - builder.registerActivity(TaskExecutionKeyActivity.class); - - + return builder; } }