Skip to content

Adding Support for Suspend / Resume Workflows #1405

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 11 commits into from
Jun 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 57 additions & 2 deletions examples/src/main/java/io/dapr/examples/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ client.raiseEvent(instanceId, "Approval", true);

Start the workflow and client using the following commands:

ex
```sh
dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.externalevent.DemoExternalEventWorker
```
Expand Down Expand Up @@ -652,4 +651,60 @@ 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
4. Each activity simulates work with a short delay for demonstration purposes


### Suspend/Resume Pattern

Workflow instances can be suspended and resumed. This example shows how to use the suspend and resume commands.

For testing the suspend and resume operations we will use the same workflow definition used by the DemoExternalEventWorkflow.

Start the workflow and client using the following commands:


<!-- STEP
name: Run Suspend/Resume workflow
match_order: none
output_match_mode: substring
expected_stdout_lines:
- "Waiting for approval..."
- "Suspending Workflow Instance"
- "Workflow Instance Status: SUSPENDED"
- "Let's resume the Workflow Instance before sending the external event"
- "Workflow Instance Status: RUNNING"
- "Now that the instance is RUNNING again, lets send the external event."
- "approval granted - do the approved action"
- "Starting Activity: io.dapr.examples.workflows.externalevent.ApproveActivity"
- "Running approval activity..."
- "approval-activity finished"
background: true
sleep: 60
timeout_seconds: 60
-->

```sh
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
```

```sh
java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.suspendresume.DemoSuspendResumeClient
```

<!-- END_STEP -->

The worker logs:
```text
== 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
== APP == 2023-11-07 16:01:23,279 {HH:mm:ss.SSS} [main] INFO io.dapr.workflows.WorkflowContext - Waiting for approval...
== APP == 2023-11-07 16:01:23,324 {HH:mm:ss.SSS} [main] INFO io.dapr.workflows.WorkflowContext - approval granted - do the approved action
== 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
== APP == 2023-11-07 16:01:23,348 {HH:mm:ss.SSS} [main] INFO i.d.e.w.e.ApproveActivity - Running approval activity...
== APP == 2023-11-07 16:01:28,410 {HH:mm:ss.SSS} [main] INFO io.dapr.workflows.WorkflowContext - approval-activity finished
```

The client log:
```text
Started a new external-event model workflow with instance ID: 23410d96-1afe-4698-9fcd-c01c1e0db255
workflow instance with ID: 23410d96-1afe-4698-9fcd-c01c1e0db255 completed.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.suspendresume;

import io.dapr.examples.workflows.externalevent.DemoExternalEventWorkflow;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;

import java.util.concurrent.TimeoutException;

public class DemoSuspendResumeClient {
/**
* The main method to start the client.
*
* @param args Input arguments (unused).
* @throws InterruptedException If program has been interrupted.
*/
public static void main(String[] args) {
try (DaprWorkflowClient client = new DaprWorkflowClient()) {
String instanceId = client.scheduleNewWorkflow(DemoExternalEventWorkflow.class);
System.out.printf("Started a new external-event workflow with instance ID: %s%n", instanceId);


System.out.printf("Suspending Workflow Instance: %s%n", instanceId );
client.suspendWorkflow(instanceId, "suspending workflow instance.");

WorkflowInstanceStatus instanceState = client.getInstanceState(instanceId, false);
assert instanceState != null;
System.out.printf("Workflow Instance Status: %s%n", instanceState.getRuntimeStatus().name() );

System.out.printf("Let's resume the Workflow Instance before sending the external event: %s%n", instanceId );
client.resumeWorkflow(instanceId, "resuming workflow instance.");

instanceState = client.getInstanceState(instanceId, false);
assert instanceState != null;
System.out.printf("Workflow Instance Status: %s%n", instanceState.getRuntimeStatus().name() );

System.out.printf("Now that the instance is RUNNING again, lets send the external event. %n");
client.raiseEvent(instanceId, "Approval", true);

client.waitForInstanceCompletion(instanceId, null, true);
System.out.printf("workflow instance with ID: %s completed.", instanceId);

} catch (TimeoutException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}
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.suspendresume;

import io.dapr.examples.workflows.externalevent.ApproveActivity;
import io.dapr.examples.workflows.externalevent.DemoExternalEventWorkflow;
import io.dapr.examples.workflows.externalevent.DenyActivity;
import io.dapr.workflows.runtime.WorkflowRuntime;
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;

public class DemoSuspendResumeWorker {
/**
* The main method of this app.
*
* @param args The port the app will listen on.
* @throws Exception An Exception.
*/
public static void main(String[] args) throws Exception {
// Register the Workflow with the builder.
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder().registerWorkflow(DemoExternalEventWorkflow.class);
builder.registerActivity(ApproveActivity.class);
builder.registerActivity(DenyActivity.class);

// Build and then start the workflow runtime pulling and executing tasks
WorkflowRuntime runtime = builder.build();
System.out.println("Start workflow runtime");
runtime.start();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.dapr.testcontainers.DaprLogLevel;
import io.dapr.workflows.client.DaprWorkflowClient;
import io.dapr.workflows.client.WorkflowInstanceStatus;
import io.dapr.workflows.client.WorkflowRuntimeStatus;
import io.dapr.workflows.runtime.WorkflowRuntime;
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -117,6 +118,36 @@ public void testWorkflows() throws Exception {
assertEquals(instanceId, workflowOutput.getWorkflowId());
}

@Test
public void testSuspendAndResumeWorkflows() throws Exception {
TestWorkflowPayload payload = new TestWorkflowPayload(new ArrayList<>());
String instanceId = workflowClient.scheduleNewWorkflow(TestWorkflow.class, payload);
workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(10), false);

workflowClient.suspendWorkflow(instanceId, "testing suspend.");


WorkflowInstanceStatus instanceState = workflowClient.getInstanceState(instanceId, false);
assertNotNull(instanceState);
assertEquals(WorkflowRuntimeStatus.SUSPENDED, instanceState.getRuntimeStatus());

workflowClient.resumeWorkflow(instanceId, "testing resume");

instanceState = workflowClient.getInstanceState(instanceId, false);
assertNotNull(instanceState);
assertEquals(WorkflowRuntimeStatus.RUNNING, instanceState.getRuntimeStatus());

workflowClient.raiseEvent(instanceId, "MoveForward", payload);

Duration timeout = Duration.ofSeconds(10);
instanceState = workflowClient.waitForInstanceCompletion(instanceId, timeout, true);

assertNotNull(instanceState);
assertEquals(WorkflowRuntimeStatus.COMPLETED, instanceState.getRuntimeStatus());

}


private TestWorkflowPayload deserialize(String value) throws JsonProcessingException {
return OBJECT_MAPPER.readValue(value, TestWorkflowPayload.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ public <T extends Workflow> String scheduleNewWorkflow(Class<T> clazz, NewWorkfl
orchestrationInstanceOptions);
}

/**
* Suspend the workflow associated with the provided instance id.
*
* @param workflowInstanceId Workflow instance id to suspend.
* @param reason reason for suspending the workflow instance.
*/
public void suspendWorkflow(String workflowInstanceId, @Nullable String reason) {
this.innerClient.suspendInstance(workflowInstanceId, reason);
}

/**
* Resume the workflow associated with the provided instance id.
*
* @param workflowInstanceId Workflow instance id to resume.
* @param reason reason for resuming the workflow instance.
*/
public void resumeWorkflow(String workflowInstanceId, @Nullable String reason) {
this.innerClient.resumeInstance(workflowInstanceId, reason);
}

/**
* Terminates the workflow associated with the provided instance id.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ public void raiseEvent() {
expectedEventName, expectedEventPayload);
}

@Test
public void suspendResumeInstance() {
String expectedArgument = "TestWorkflowInstanceId";
client.suspendWorkflow(expectedArgument, "suspending workflow instance");
client.resumeWorkflow(expectedArgument, "resuming workflow instance");
verify(mockInnerClient, times(1)).suspendInstance(expectedArgument,
"suspending workflow instance");
verify(mockInnerClient, times(1)).resumeInstance(expectedArgument,
"resuming workflow instance");
}

@Test
public void purgeInstance() {
String expectedArgument = "TestWorkflowInstanceId";
Expand Down
Loading
Loading