Skip to content

Commit dcaca77

Browse files
salaboycicoyle
andauthored
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]>
1 parent e13f934 commit dcaca77

File tree

11 files changed

+517
-7
lines changed

11 files changed

+517
-7
lines changed

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

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,6 @@ client.raiseEvent(instanceId, "Approval", true);
420420

421421
Start the workflow and client using the following commands:
422422

423-
ex
424423
```sh
425424
dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.externalevent.DemoExternalEventWorker
426425
```
@@ -652,4 +651,60 @@ Key Points:
652651
1. Each successful booking step adds its compensation action to an ArrayList
653652
2. If an error occurs, the list of compensations is reversed and executed in reverse order
654653
3. The workflow ensures that all resources are properly cleaned up even if the process fails
655-
4. Each activity simulates work with a short delay for demonstration purposes
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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.suspendresume;
15+
16+
import io.dapr.examples.workflows.externalevent.DemoExternalEventWorkflow;
17+
import io.dapr.workflows.client.DaprWorkflowClient;
18+
import io.dapr.workflows.client.WorkflowInstanceStatus;
19+
20+
import java.util.concurrent.TimeoutException;
21+
22+
public class DemoSuspendResumeClient {
23+
/**
24+
* The main method to start the client.
25+
*
26+
* @param args Input arguments (unused).
27+
* @throws InterruptedException If program has been interrupted.
28+
*/
29+
public static void main(String[] args) {
30+
try (DaprWorkflowClient client = new DaprWorkflowClient()) {
31+
String instanceId = client.scheduleNewWorkflow(DemoExternalEventWorkflow.class);
32+
System.out.printf("Started a new external-event workflow with instance ID: %s%n", instanceId);
33+
34+
35+
System.out.printf("Suspending Workflow Instance: %s%n", instanceId );
36+
client.suspendWorkflow(instanceId, "suspending workflow instance.");
37+
38+
WorkflowInstanceStatus instanceState = client.getInstanceState(instanceId, false);
39+
assert instanceState != null;
40+
System.out.printf("Workflow Instance Status: %s%n", instanceState.getRuntimeStatus().name() );
41+
42+
System.out.printf("Let's resume the Workflow Instance before sending the external event: %s%n", instanceId );
43+
client.resumeWorkflow(instanceId, "resuming workflow instance.");
44+
45+
instanceState = client.getInstanceState(instanceId, false);
46+
assert instanceState != null;
47+
System.out.printf("Workflow Instance Status: %s%n", instanceState.getRuntimeStatus().name() );
48+
49+
System.out.printf("Now that the instance is RUNNING again, lets send the external event. %n");
50+
client.raiseEvent(instanceId, "Approval", true);
51+
52+
client.waitForInstanceCompletion(instanceId, null, true);
53+
System.out.printf("workflow instance with ID: %s completed.", instanceId);
54+
55+
} catch (TimeoutException | InterruptedException e) {
56+
throw new RuntimeException(e);
57+
}
58+
}
59+
}
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.suspendresume;
15+
16+
import io.dapr.examples.workflows.externalevent.ApproveActivity;
17+
import io.dapr.examples.workflows.externalevent.DemoExternalEventWorkflow;
18+
import io.dapr.examples.workflows.externalevent.DenyActivity;
19+
import io.dapr.workflows.runtime.WorkflowRuntime;
20+
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
21+
22+
public class DemoSuspendResumeWorker {
23+
/**
24+
* The main method of this app.
25+
*
26+
* @param args The port the app will listen on.
27+
* @throws Exception An Exception.
28+
*/
29+
public static void main(String[] args) throws Exception {
30+
// Register the Workflow with the builder.
31+
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder().registerWorkflow(DemoExternalEventWorkflow.class);
32+
builder.registerActivity(ApproveActivity.class);
33+
builder.registerActivity(DenyActivity.class);
34+
35+
// Build and then start the workflow runtime pulling and executing tasks
36+
WorkflowRuntime runtime = builder.build();
37+
System.out.println("Start workflow runtime");
38+
runtime.start();
39+
}
40+
}

sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.dapr.testcontainers.DaprLogLevel;
2121
import io.dapr.workflows.client.DaprWorkflowClient;
2222
import io.dapr.workflows.client.WorkflowInstanceStatus;
23+
import io.dapr.workflows.client.WorkflowRuntimeStatus;
2324
import io.dapr.workflows.runtime.WorkflowRuntime;
2425
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
2526
import org.junit.jupiter.api.BeforeEach;
@@ -117,6 +118,36 @@ public void testWorkflows() throws Exception {
117118
assertEquals(instanceId, workflowOutput.getWorkflowId());
118119
}
119120

121+
@Test
122+
public void testSuspendAndResumeWorkflows() throws Exception {
123+
TestWorkflowPayload payload = new TestWorkflowPayload(new ArrayList<>());
124+
String instanceId = workflowClient.scheduleNewWorkflow(TestWorkflow.class, payload);
125+
workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(10), false);
126+
127+
workflowClient.suspendWorkflow(instanceId, "testing suspend.");
128+
129+
130+
WorkflowInstanceStatus instanceState = workflowClient.getInstanceState(instanceId, false);
131+
assertNotNull(instanceState);
132+
assertEquals(WorkflowRuntimeStatus.SUSPENDED, instanceState.getRuntimeStatus());
133+
134+
workflowClient.resumeWorkflow(instanceId, "testing resume");
135+
136+
instanceState = workflowClient.getInstanceState(instanceId, false);
137+
assertNotNull(instanceState);
138+
assertEquals(WorkflowRuntimeStatus.RUNNING, instanceState.getRuntimeStatus());
139+
140+
workflowClient.raiseEvent(instanceId, "MoveForward", payload);
141+
142+
Duration timeout = Duration.ofSeconds(10);
143+
instanceState = workflowClient.waitForInstanceCompletion(instanceId, timeout, true);
144+
145+
assertNotNull(instanceState);
146+
assertEquals(WorkflowRuntimeStatus.COMPLETED, instanceState.getRuntimeStatus());
147+
148+
}
149+
150+
120151
private TestWorkflowPayload deserialize(String value) throws JsonProcessingException {
121152
return OBJECT_MAPPER.readValue(value, TestWorkflowPayload.class);
122153
}

sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,26 @@ public <T extends Workflow> String scheduleNewWorkflow(Class<T> clazz, NewWorkfl
129129
orchestrationInstanceOptions);
130130
}
131131

132+
/**
133+
* Suspend the workflow associated with the provided instance id.
134+
*
135+
* @param workflowInstanceId Workflow instance id to suspend.
136+
* @param reason reason for suspending the workflow instance.
137+
*/
138+
public void suspendWorkflow(String workflowInstanceId, @Nullable String reason) {
139+
this.innerClient.suspendInstance(workflowInstanceId, reason);
140+
}
141+
142+
/**
143+
* Resume the workflow associated with the provided instance id.
144+
*
145+
* @param workflowInstanceId Workflow instance id to resume.
146+
* @param reason reason for resuming the workflow instance.
147+
*/
148+
public void resumeWorkflow(String workflowInstanceId, @Nullable String reason) {
149+
this.innerClient.resumeInstance(workflowInstanceId, reason);
150+
}
151+
132152
/**
133153
* Terminates the workflow associated with the provided instance id.
134154
*

sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,17 @@ public void raiseEvent() {
217217
expectedEventName, expectedEventPayload);
218218
}
219219

220+
@Test
221+
public void suspendResumeInstance() {
222+
String expectedArgument = "TestWorkflowInstanceId";
223+
client.suspendWorkflow(expectedArgument, "suspending workflow instance");
224+
client.resumeWorkflow(expectedArgument, "resuming workflow instance");
225+
verify(mockInnerClient, times(1)).suspendInstance(expectedArgument,
226+
"suspending workflow instance");
227+
verify(mockInnerClient, times(1)).resumeInstance(expectedArgument,
228+
"resuming workflow instance");
229+
}
230+
220231
@Test
221232
public void purgeInstance() {
222233
String expectedArgument = "TestWorkflowInstanceId";

0 commit comments

Comments
 (0)