Skip to content

Compensation example for Workflows #1333

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
May 29, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
de243ff
add basic compensation example for wf
cicoyle May 5, 2025
1d74fbc
update commands to run + wf id
cicoyle May 12, 2025
312c99c
update readme + add mechanical markdown
cicoyle May 19, 2025
8f648b2
Merge branch 'master' into feat-wf-compensating-activities
cicoyle May 19, 2025
417a0af
fix import
cicoyle May 19, 2025
3f8cab0
fix mechanical markdown + add how to test it locally
cicoyle May 19, 2025
457dd76
move compensation example readme to workflows readme
cicoyle May 19, 2025
7fbc50e
Merge branch 'master' into feat-wf-compensating-activities
cicoyle May 21, 2025
b5829bf
Update BookCarActivity.java
artur-ciocanu May 22, 2025
94ed1a9
Update BookFlightActivity.java
artur-ciocanu May 22, 2025
5eba626
Update BookHotelActivity.java
artur-ciocanu May 22, 2025
5f227ad
Update BookTripClient.java
artur-ciocanu May 22, 2025
2e4448b
Update BookTripWorker.java
artur-ciocanu May 22, 2025
4beb458
Update BookTripWorkflow.java
artur-ciocanu May 22, 2025
e272c14
Update CancelCarActivity.java
artur-ciocanu May 22, 2025
cd80b1a
Update CancelFlightActivity.java
artur-ciocanu May 22, 2025
5dbf3dc
Update CancelHotelActivity.java
artur-ciocanu May 22, 2025
828f5eb
Merge branch 'master' into feat-wf-compensating-activities
artur-ciocanu May 22, 2025
c6b507f
Merge branch 'master' into feat-wf-compensating-activities
artur-ciocanu May 23, 2025
2b0c5a0
Merge branch 'master' into feat-wf-compensating-activities
artur-ciocanu May 27, 2025
7ecf00e
Merge branch 'master' into feat-wf-compensating-activities
cicoyle May 28, 2025
6d49de0
Merge branch 'master' into feat-wf-compensating-activities
artur-ciocanu May 28, 2025
732d4f3
add retry IT tests and catch TaskFailedException
cicoyle May 29, 2025
8a27e79
add test for no compensation if successful and assert attempts
cicoyle May 29, 2025
8fc18f9
update mechanical markdown
cicoyle May 29, 2025
063e9ea
Merge branch 'master' into feat-wf-compensating-activities
cicoyle May 29, 2025
958de58
Merge branch 'master' into feat-wf-compensating-activities
cicoyle May 29, 2025
2528a21
add back pubsub... but this should be removed long term
cicoyle May 29, 2025
da76513
try adding waitforsidecar
cicoyle May 29, 2025
e9455ac
rm tests from examples pr
cicoyle May 29, 2025
13a0891
reset unintended changes
cicoyle May 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/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
<!-- STEP
name: Run example
output_match_mode: substring
expected_stdout_lines:
- "Starting workflow: io.dapr.examples.workflows.compensation.BookTripWorkflow"
...
background: true
timeout_seconds: 60
-->
```

### Pull Requests

Expand Down
120 changes: 117 additions & 3 deletions examples/src/main/java/io/dapr/examples/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<String> 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.

<!-- STEP
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"
- "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
-->

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
```
<!-- END_STEP -->

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
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading
Loading