Skip to content

Commit e297d01

Browse files
cicoyleartur-ciocanusalaboy
authored
[1.15] Cherrypick Compensation PR + Suspend/Resume PR (#1415)
* Compensation example for Workflows (#1333) * add basic compensation example for wf Signed-off-by: Cassandra Coyle <[email protected]> * update commands to run + wf id Signed-off-by: Cassandra Coyle <[email protected]> * update readme + add mechanical markdown Signed-off-by: Cassandra Coyle <[email protected]> * fix import Signed-off-by: Cassandra Coyle <[email protected]> * fix mechanical markdown + add how to test it locally Signed-off-by: Cassandra Coyle <[email protected]> * move compensation example readme to workflows readme Signed-off-by: Cassandra Coyle <[email protected]> * Update BookCarActivity.java Signed-off-by: artur-ciocanu <[email protected]> * Update BookFlightActivity.java Signed-off-by: artur-ciocanu <[email protected]> * Update BookHotelActivity.java Signed-off-by: artur-ciocanu <[email protected]> * Update BookTripClient.java Signed-off-by: artur-ciocanu <[email protected]> * Update BookTripWorker.java Signed-off-by: artur-ciocanu <[email protected]> * Update BookTripWorkflow.java Signed-off-by: artur-ciocanu <[email protected]> * Update CancelCarActivity.java Signed-off-by: artur-ciocanu <[email protected]> * Update CancelFlightActivity.java Signed-off-by: artur-ciocanu <[email protected]> * Update CancelHotelActivity.java Signed-off-by: artur-ciocanu <[email protected]> * add retry IT tests and catch TaskFailedException Signed-off-by: Cassandra Coyle <[email protected]> * add test for no compensation if successful and assert attempts Signed-off-by: Cassandra Coyle <[email protected]> * update mechanical markdown Signed-off-by: Cassandra Coyle <[email protected]> * add back pubsub... but this should be removed long term Signed-off-by: Cassandra Coyle <[email protected]> * try adding waitforsidecar Signed-off-by: Cassandra Coyle <[email protected]> * rm tests from examples pr Signed-off-by: Cassandra Coyle <[email protected]> * reset unintended changes Signed-off-by: Cassandra Coyle <[email protected]> --------- Signed-off-by: Cassandra Coyle <[email protected]> Signed-off-by: artur-ciocanu <[email protected]> Co-authored-by: artur-ciocanu <[email protected]> * Adding Support for Suspend / Resume Workflows (#1405) * adding IT test Signed-off-by: salaboy <[email protected]> * adding initial version of suspend/resume example Signed-off-by: salaboy <[email protected]> * updating README Signed-off-by: salaboy <[email protected]> * Update README.md Signed-off-by: salaboy <[email protected]> * following Javi's suggestion Signed-off-by: salaboy <[email protected]> * fixing wrong year in headers Signed-off-by: salaboy <[email protected]> * fixing paths in one more README.md file Signed-off-by: salaboy <[email protected]> * adding output validation Signed-off-by: salaboy <[email protected]> * adding missing port Signed-off-by: salaboy <[email protected]> * fixing check conditions Signed-off-by: salaboy <[email protected]> --------- Signed-off-by: salaboy <[email protected]> Co-authored-by: Cassie Coyle <[email protected]> --------- Signed-off-by: Cassandra Coyle <[email protected]> Signed-off-by: artur-ciocanu <[email protected]> Signed-off-by: salaboy <[email protected]> Co-authored-by: artur-ciocanu <[email protected]> Co-authored-by: salaboy <[email protected]>
1 parent 1a69298 commit e297d01

File tree

21 files changed

+1813
-5
lines changed

21 files changed

+1813
-5
lines changed

CONTRIBUTING.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,48 @@ This section describes the guidelines for contributing code / docs to Dapr.
5454
### Things to consider when adding new API to SDK
5555

5656
1. All the new API's go under [dapr-sdk maven package](https://github.com/dapr/java-sdk/tree/master/sdk)
57-
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)
57+
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)
58+
59+
#### Mechanical Markdown
60+
61+
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.
62+
63+
To test mechanical markdown locally:
64+
65+
1. Install the package:
66+
```bash
67+
pip3 install mechanical-markdown
68+
```
69+
70+
2. Run the test from the respective examples README directory, for example:
71+
```bash
72+
cd examples
73+
mm.py ./src/main/java/io/dapr/examples/workflows/README.md
74+
```
75+
76+
The test will:
77+
- Parse the STEP markers in the README
78+
- Execute the commands specified in the markers
79+
- Compare the actual output with the expected output
80+
- Report any mismatches
81+
82+
When writing STEP markers:
83+
- Use `output_match_mode: substring` for flexible matching
84+
- Quote strings containing special YAML characters (like `:`, `*`, `'`)
85+
- Set appropriate timeouts for long-running examples
86+
87+
Example STEP marker:
88+
```yaml
89+
<!-- STEP
90+
name: Run example
91+
output_match_mode: substring
92+
expected_stdout_lines:
93+
- "Starting workflow: io.dapr.examples.workflows.compensation.BookTripWorkflow"
94+
...
95+
background: true
96+
timeout_seconds: 60
97+
-->
98+
```
5899

59100
### Pull Requests
60101

examples/src/main/java/io/dapr/examples/workflows/README.md

Lines changed: 172 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ Those examples contain the following workflow patterns:
5151
2. [Fan-out/Fan-in Pattern](#fan-outfan-in-pattern)
5252
3. [Continue As New Pattern](#continue-as-new-pattern)
5353
4. [External Event Pattern](#external-event-pattern)
54-
5. [child-workflow Pattern](#child-workflow-pattern)
54+
5. [Child-workflow Pattern](#child-workflow-pattern)
55+
6. [Compensation Pattern](#compensation-pattern)
5556

5657
### Chaining Pattern
5758
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 --
353354
```
354355
```sh
355356
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.continueasnew.DemoContinueAsNewClient
356-
````
357+
```
357358

358359
You will see the logs from worker showing the `CleanUpActivity` is invoked every 10 seconds after previous one is finished:
359360
```text
@@ -419,7 +420,6 @@ client.raiseEvent(instanceId, "Approval", true);
419420

420421
Start the workflow and client using the following commands:
421422

422-
ex
423423
```sh
424424
dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.externalevent.DemoExternalEventWorker
425425
```
@@ -444,7 +444,7 @@ Started a new external-event model workflow with instance ID: 23410d96-1afe-4698
444444
workflow instance with ID: 23410d96-1afe-4698-9fcd-c01c1e0db255 completed.
445445
```
446446

447-
### child-workflow Pattern
447+
### Child-workflow Pattern
448448
The child-workflow pattern allows you to call a workflow from another workflow.
449449

450450
The `DemoWorkflow` class defines the workflow. It calls a child-workflow `DemoChildWorkflow` to do the work. See the code snippet below:
@@ -540,3 +540,171 @@ The log from client:
540540
Started a new child-workflow model workflow with instance ID: c2fb9c83-435b-4b55-bdf1-833b39366cfb
541541
workflow instance with ID: c2fb9c83-435b-4b55-bdf1-833b39366cfb completed with result: !wolfkroW rpaD olleH
542542
```
543+
544+
### Compensation Pattern
545+
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.
546+
547+
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.
548+
549+
The `BookTripWorkflow` class defines the workflow. It orchestrates the booking process and handles compensation if any step fails. See the code snippet below:
550+
```java
551+
public class BookTripWorkflow extends Workflow {
552+
@Override
553+
public WorkflowStub create() {
554+
return ctx -> {
555+
List<String> compensations = new ArrayList<>();
556+
557+
try {
558+
// Book flight
559+
String flightResult = ctx.callActivity(BookFlightActivity.class.getName(), String.class).await();
560+
ctx.getLogger().info("Flight booking completed: " + flightResult);
561+
compensations.add(CancelFlightActivity.class.getName());
562+
563+
// Book hotel
564+
String hotelResult = ctx.callActivity(BookHotelActivity.class.getName(), String.class).await();
565+
ctx.getLogger().info("Hotel booking completed: " + hotelResult);
566+
compensations.add(CancelHotelActivity.class.getName());
567+
568+
// Book car
569+
String carResult = ctx.callActivity(BookCarActivity.class.getName(), String.class).await();
570+
ctx.getLogger().info("Car booking completed: " + carResult);
571+
compensations.add(CancelCarActivity.class.getName());
572+
573+
} catch (Exception e) {
574+
ctx.getLogger().info("******** executing compensation logic ********");
575+
// Execute compensations in reverse order
576+
Collections.reverse(compensations);
577+
for (String compensation : compensations) {
578+
try {
579+
ctx.callActivity(compensation, String.class).await();
580+
} catch (Exception ex) {
581+
ctx.getLogger().error("Error during compensation: " + ex.getMessage());
582+
}
583+
}
584+
ctx.complete("Workflow failed, compensation applied");
585+
return;
586+
}
587+
ctx.complete("All bookings completed successfully");
588+
};
589+
}
590+
}
591+
```
592+
593+
Each activity class (`BookFlightActivity`, `BookHotelActivity`, `BookCarActivity`) implements the booking logic, while their corresponding compensation activities (`CancelFlightActivity`, `CancelHotelActivity`, `CancelCarActivity`) implement the cancellation logic.
594+
595+
<!-- STEP
596+
name: Run Compensation Pattern workflow worker
597+
match_order: none
598+
output_match_mode: substring
599+
expected_stdout_lines:
600+
- "Registered Workflow: BookTripWorkflow"
601+
- "Registered Activity: BookFlightActivity"
602+
- "Registered Activity: CancelFlightActivity"
603+
- "Registered Activity: BookHotelActivity"
604+
- "Registered Activity: CancelHotelActivity"
605+
- "Registered Activity: BookCarActivity"
606+
- "Registered Activity: CancelCarActivity"
607+
- "Successfully built dapr workflow runtime"
608+
- "Start workflow runtime"
609+
- "Durable Task worker is connecting to sidecar at 127.0.0.1:50001."
610+
611+
- "Starting Workflow: io.dapr.examples.workflows.compensation.BookTripWorkflow"
612+
- "Starting Activity: io.dapr.examples.workflows.compensation.BookFlightActivity"
613+
- "Activity completed with result: Flight booked successfully"
614+
- "Flight booking completed: Flight booked successfully"
615+
- "Starting Activity: io.dapr.examples.workflows.compensation.BookHotelActivity"
616+
- "Simulating hotel booking process..."
617+
- "Activity completed with result: Hotel booked successfully"
618+
- "Hotel booking completed: Hotel booked successfully"
619+
- "Starting Activity: io.dapr.examples.workflows.compensation.BookCarActivity"
620+
- "Forcing Failure to trigger compensation for activity: io.dapr.examples.workflows.compensation.BookCarActivity"
621+
- "******** executing compensation logic ********"
622+
- "Activity failed: Task 'io.dapr.examples.workflows.compensation.BookCarActivity' (#2) failed with an unhandled exception: Failed to book car"
623+
- "Starting Activity: io.dapr.examples.workflows.compensation.CancelHotelActivity"
624+
- "Activity completed with result: Hotel canceled successfully"
625+
- "Starting Activity: io.dapr.examples.workflows.compensation.CancelFlightActivity"
626+
- "Activity completed with result: Flight canceled successfully"
627+
background: true
628+
sleep: 60
629+
timeout_seconds: 60
630+
-->
631+
632+
Execute the following script in order to run the BookTripWorker:
633+
```sh
634+
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
635+
```
636+
637+
Once running, execute the following script to run the BookTripClient:
638+
```sh
639+
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.compensation.BookTripClient
640+
```
641+
<!-- END_STEP -->
642+
643+
The output demonstrates:
644+
1. The workflow starts and successfully books a flight
645+
2. Then successfully books a hotel
646+
3. When attempting to book a car, it fails (intentionally)
647+
4. The compensation logic triggers, canceling the hotel and flight in reverse order
648+
5. The workflow completes with a status indicating the compensation was applied
649+
650+
Key Points:
651+
1. Each successful booking step adds its compensation action to an ArrayList
652+
2. If an error occurs, the list of compensations is reversed and executed in reverse order
653+
3. The workflow ensures that all resources are properly cleaned up even if the process fails
654+
4. Each activity simulates work with a short delay for demonstration purposes
655+
656+
657+
### Suspend/Resume Pattern
658+
659+
Workflow instances can be suspended and resumed. This example shows how to use the suspend and resume commands.
660+
661+
For testing the suspend and resume operations we will use the same workflow definition used by the DemoExternalEventWorkflow.
662+
663+
Start the workflow and client using the following commands:
664+
665+
666+
<!-- STEP
667+
name: Run Suspend/Resume workflow
668+
match_order: none
669+
output_match_mode: substring
670+
expected_stdout_lines:
671+
- "Waiting for approval..."
672+
- "Suspending Workflow Instance"
673+
- "Workflow Instance Status: SUSPENDED"
674+
- "Let's resume the Workflow Instance before sending the external event"
675+
- "Workflow Instance Status: RUNNING"
676+
- "Now that the instance is RUNNING again, lets send the external event."
677+
- "approval granted - do the approved action"
678+
- "Starting Activity: io.dapr.examples.workflows.externalevent.ApproveActivity"
679+
- "Running approval activity..."
680+
- "approval-activity finished"
681+
background: true
682+
sleep: 60
683+
timeout_seconds: 60
684+
-->
685+
686+
```sh
687+
dapr run --app-id demoworkflowworker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.suspendresume.DemoSuspendResumeWorker
688+
```
689+
690+
```sh
691+
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.suspendresume.DemoSuspendResumeClient
692+
```
693+
694+
<!-- END_STEP -->
695+
696+
The worker logs:
697+
```text
698+
== APP == 2023-11-07 16:01:23,279 {HH:mm:ss.SSS} [main] INFO io.dapr.workflows.WorkflowContext - Starting Workflow: io.dapr.examples.workflows.suspendresume.DemoExternalEventWorkflow
699+
== APP == 2023-11-07 16:01:23,279 {HH:mm:ss.SSS} [main] INFO io.dapr.workflows.WorkflowContext - Waiting for approval...
700+
== APP == 2023-11-07 16:01:23,324 {HH:mm:ss.SSS} [main] INFO io.dapr.workflows.WorkflowContext - approval granted - do the approved action
701+
== APP == 2023-11-07 16:01:23,348 {HH:mm:ss.SSS} [main] INFO i.d.e.w.e.ApproveActivity - Starting Activity: io.dapr.examples.workflows.externalevent.ApproveActivity
702+
== APP == 2023-11-07 16:01:23,348 {HH:mm:ss.SSS} [main] INFO i.d.e.w.e.ApproveActivity - Running approval activity...
703+
== APP == 2023-11-07 16:01:28,410 {HH:mm:ss.SSS} [main] INFO io.dapr.workflows.WorkflowContext - approval-activity finished
704+
```
705+
706+
The client log:
707+
```text
708+
Started a new external-event model workflow with instance ID: 23410d96-1afe-4698-9fcd-c01c1e0db255
709+
workflow instance with ID: 23410d96-1afe-4698-9fcd-c01c1e0db255 completed.
710+
```
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import java.util.concurrent.TimeUnit;
22+
23+
public class BookCarActivity implements WorkflowActivity {
24+
private static final Logger logger = LoggerFactory.getLogger(BookCarActivity.class);
25+
26+
@Override
27+
public String run(WorkflowActivityContext ctx) {
28+
logger.info("Starting Activity: " + ctx.getName());
29+
30+
// Simulate work
31+
try {
32+
TimeUnit.SECONDS.sleep(2);
33+
} catch (InterruptedException e) {
34+
throw new RuntimeException(e);
35+
}
36+
37+
logger.info("Forcing Failure to trigger compensation for activity: " + ctx.getName());
38+
39+
// force the compensation
40+
throw new RuntimeException("Failed to book car");
41+
}
42+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
import java.util.concurrent.TimeUnit;
22+
23+
public class BookFlightActivity implements WorkflowActivity {
24+
private static final Logger logger = LoggerFactory.getLogger(BookFlightActivity.class);
25+
26+
@Override
27+
public String run(WorkflowActivityContext ctx) {
28+
logger.info("Starting Activity: " + ctx.getName());
29+
30+
// Simulate work
31+
try {
32+
TimeUnit.SECONDS.sleep(2);
33+
} catch (InterruptedException e) {
34+
throw new RuntimeException(e);
35+
}
36+
37+
String result = "Flight booked successfully";
38+
logger.info("Activity completed with result: " + result);
39+
return result;
40+
}
41+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.compensation;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
import org.slf4j.Logger;
19+
import org.slf4j.LoggerFactory;
20+
21+
public class BookHotelActivity implements WorkflowActivity {
22+
private static final Logger logger = LoggerFactory.getLogger(BookHotelActivity.class);
23+
24+
@Override
25+
public String run(WorkflowActivityContext ctx) {
26+
logger.info("Starting Activity: " + ctx.getName());
27+
logger.info("Simulating hotel booking process...");
28+
29+
// Simulate some work
30+
try {
31+
Thread.sleep(2000);
32+
} catch (InterruptedException e) {
33+
Thread.currentThread().interrupt();
34+
}
35+
36+
String result = "Hotel booked successfully";
37+
logger.info("Activity completed with result: " + result);
38+
return result;
39+
}
40+
}

0 commit comments

Comments
 (0)