From 959dc19d0c7c1fe0ed38c4765ea27e0deecfe2d1 Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 6 Jun 2025 10:03:08 +0100 Subject: [PATCH 01/10] adding IT test Signed-off-by: salaboy --- .../workflows/DaprWorkflowsIT.java | 31 ++++++++++++ .../workflows/client/DaprWorkflowClient.java | 20 ++++++++ .../client/DaprWorkflowClientTest.java | 11 +++++ .../wfp/WorkflowPatternsRestController.java | 36 ++++++++++++++ .../suspendresume/PerformTaskActivity.java | 43 ++++++++++++++++ .../suspendresume/SuspendResumeWorkflow.java | 40 +++++++++++++++ .../wfp/WorkflowPatternsAppTests.java | 49 +++++++++++++++++-- 7 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java create mode 100644 spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java index 5b019f831..19fe8f986 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java @@ -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; @@ -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); } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java index ab46dff79..b24c8bcc9 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/client/DaprWorkflowClient.java @@ -129,6 +129,26 @@ public String scheduleNewWorkflow(Class 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. * diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java index 3ad66877c..55f7c9fdd 100644 --- a/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java +++ b/sdk-workflows/src/test/java/io/dapr/workflows/client/DaprWorkflowClientTest.java @@ -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"; diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java index ddffdb018..bba757a43 100644 --- a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java @@ -24,6 +24,7 @@ import io.dapr.springboot.examples.wfp.fanoutin.Result; import io.dapr.springboot.examples.wfp.remoteendpoint.Payload; import io.dapr.springboot.examples.wfp.remoteendpoint.RemoteEndpointWorkflow; +import io.dapr.springboot.examples.wfp.suspendresume.SuspendResumeWorkflow; import io.dapr.workflows.client.DaprWorkflowClient; import io.dapr.workflows.client.WorkflowInstanceStatus; import org.slf4j.Logger; @@ -153,4 +154,39 @@ public Payload remoteEndpoint(@RequestBody Payload payload) return workflowInstanceStatus.readOutputAs(Payload.class); } + @PostMapping("wfp/suspendresume") + public String suspendResume(@RequestParam("orderId") String orderId) { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(SuspendResumeWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + ordersToApprove.put(orderId, instanceId); + return instanceId; + } + + @PostMapping("wfp/suspendresume-suspend") + public String suspendResumeExecuteSuspend(@RequestParam("orderId") String orderId) { + String instanceId = ordersToApprove.get(orderId); + daprWorkflowClient.suspendWorkflow(instanceId, "testing suspend"); + WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(instanceId, false); + return instanceState.getRuntimeStatus().name(); + } + + @PostMapping("wfp/suspendresume-resume") + public String suspendResumeExecuteResume(@RequestParam("orderId") String orderId) { + String instanceId = ordersToApprove.get(orderId); + daprWorkflowClient.resumeWorkflow(instanceId, "testing resume"); + WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(instanceId, false); + return instanceState.getRuntimeStatus().name(); + } + + + @PostMapping("wfp/suspendresume-continue") + public Decision suspendResumeContinue(@RequestParam("orderId") String orderId, @RequestParam("decision") Boolean decision) + throws TimeoutException { + String instanceId = ordersToApprove.get(orderId); + logger.info("Workflow instance " + instanceId + " continue"); + daprWorkflowClient.raiseEvent(instanceId, "Approval", decision); + WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient + .waitForInstanceCompletion(instanceId, null, true); + return workflowInstanceStatus.readOutputAs(Decision.class); + } } diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java new file mode 100644 index 000000000..8e4b6ac26 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/PerformTaskActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023 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.springboot.examples.wfp.suspendresume; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class PerformTaskActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(PerformTaskActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + logger.info("Running activity..."); + //Sleeping for 5 seconds to simulate long running operation + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + logger.info("Completing activity..."); + + return "OK"; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java new file mode 100644 index 000000000..ca8154b8a --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/suspendresume/SuspendResumeWorkflow.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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.springboot.examples.wfp.suspendresume; + +import io.dapr.springboot.examples.wfp.externalevent.Decision; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class SuspendResumeWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + ctx.callActivity(PerformTaskActivity.class.getName(), String.class).await(); + + ctx.getLogger().info("Waiting for approval..."); + Boolean approved = ctx.waitForExternalEvent("Approval", boolean.class).await(); + + ctx.getLogger().info("approval-event arrived"); + + ctx.callActivity(PerformTaskActivity.class.getName(), String.class).await(); + + ctx.complete(new Decision(approved)); + }; + } +} diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java index 625a621a6..da7f53744 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java @@ -13,10 +13,10 @@ package io.dapr.springboot.examples.wfp; -import io.dapr.client.DaprClient; import io.dapr.springboot.DaprAutoConfiguration; import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; import io.dapr.springboot.examples.wfp.remoteendpoint.Payload; +import io.dapr.workflows.client.WorkflowRuntimeStatus; import io.github.microcks.testcontainers.MicrocksContainersEnsemble; import io.restassured.RestAssured; import io.restassured.http.ContentType; @@ -32,15 +32,13 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; @SpringBootTest(classes = {TestWorkflowPatternsApplication.class, DaprTestContainersConfig.class, DaprAutoConfiguration.class, }, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) class WorkflowPatternsAppTests { - @Autowired - private DaprClient daprClient; - @Autowired private MicrocksContainersEnsemble ensemble; @@ -160,4 +158,47 @@ void testRemoteEndpoint() { .getServiceInvocationsCount("API Payload Processor", "1.0.0")); } + @Test + void testSuspendResume() { + + String instanceId = given() + .queryParam("orderId", "123") + .when() + .post("/wfp/suspendresume") + .then() + .statusCode(200).extract().asString(); + + assertNotNull(instanceId); + + // The workflow is waiting on an event, let's suspend the workflow + String state = given() + .queryParam("orderId", "123") + .when() + .post("/wfp/suspendresume-suspend") + .then() + .statusCode(200).extract().asString(); + + assertEquals(WorkflowRuntimeStatus.SUSPENDED.name(), state); + + // The let's resume the suspended workflow and check the state + state = given() + .queryParam("orderId", "123") + .when() + .post("/wfp/suspendresume-resume") + .then() + .statusCode(200).extract().asString(); + + assertEquals(WorkflowRuntimeStatus.RUNNING.name(), state); + + // Now complete the workflow by sending an event + given() + .queryParam("orderId", "123") + .queryParam("decision", false) + .when() + .post("/wfp/suspendresume-continue") + .then() + .statusCode(200).body("approved", equalTo(false)); + + } + } From 49fad690ad120cb948c3be0bf81543101326f14a Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 6 Jun 2025 12:18:15 +0100 Subject: [PATCH 02/10] adding initial version of suspend/resume example Signed-off-by: salaboy --- .../java/io/dapr/examples/workflows/README.md | 36 +++- .../DemoSuspendResumeClient.java | 59 +++++++ .../DemoSuspendResumeWorker.java | 40 +++++ spring-boot-examples/workflows/README.md | 155 ++++++++++++++++++ 4 files changed, 288 insertions(+), 2 deletions(-) create mode 100644 examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeClient.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeWorker.java 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 4d9c057f2..d0749e1fb 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -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 ``` @@ -652,4 +651,37 @@ 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 +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: + +```sh +dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- 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 +``` + +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. +``` \ No newline at end of file diff --git a/examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeClient.java b/examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeClient.java new file mode 100644 index 000000000..e25bf6792 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeClient.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 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); + } + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeWorker.java b/examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeWorker.java new file mode 100644 index 000000000..858767daa --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/suspendresume/DemoSuspendResumeWorker.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 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(); + } +} diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md index 738c41a17..160ec9663 100644 --- a/spring-boot-examples/workflows/README.md +++ b/spring-boot-examples/workflows/README.md @@ -5,6 +5,7 @@ This application allows you to run different [workflow patterns](https://docs.da - Parent/Child Workflows - Continue workflow by sending External Events - Fan Out/In activities for parallel execution +- Suspend/Resume workflows ## Running these examples from source code @@ -392,6 +393,160 @@ i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished io.dapr.workflows.WorkflowContext : Workflow finished with result: 60 ``` +### Suspend/Resume Workflow example + +In this example we start a workflow that executes and activity and then wait for an event. While the workflow instance +is waiting for the event we execute a suspend workflow operation. Once we check the state of the instance a resume +operation is executed. + +To start the workflow you can run: + + + + +```sh +curl -X POST "localhost:8080/wfp/suspendresume?orderId=123" -H 'Content-Type: application/json' +``` + + + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.suspendresume.SuspendResumeWorkflow +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 2de2b968-900a-4f5b-9092-b26aefbfc6b3 started +i.d.s.e.w.s.PerformTaskActivity : Starting Activity: io.dapr.springboot.examples.wfp.suspendresume.PerformTaskActivity +i.d.s.e.w.s.PerformTaskActivity : Running activity... +i.d.s.e.w.s.PerformTaskActivity : Completing activity... +io.dapr.workflows.WorkflowContext : Waiting for approval... + +``` + +You should see the Workflow ID that was created, in this example you don't need to remember this id, +as you can use the orderId to find the right instance. +When you are ready to approve the order you can send the following request: + + + + +To send the event you can run: + +```sh +curl -X POST "localhost:8080/wfp/suspendresume-suspend?orderId=123" -H 'Content-Type: application/json' +``` + + + +Let's suspend the workflow instance by sending the following request: + + + + +To send the event you can run: + +```sh +curl -X POST "localhost:8080/wfp/suspendresume-suspend?orderId=123" -H 'Content-Type: application/json' +``` + + + +You should see the output of the requests: + +```sh +SUSPENDED +``` + +Now, let's resume the workflow instance: + + + + +To send the event you can run: + +```sh +curl -X POST "localhost:8080/wfp/suspendresume-resume?orderId=123" -H 'Content-Type: application/json' +``` + + + +You should see the output of the requests: + +```sh +RUNNING +``` + +Now, let's send the event that the instance is waiting to validate that the workflow complete after +being suspended and resumed. + + + + +To send the event you can run: + +```sh +curl -X POST "localhost:8080/wfp/suspendresume-continue?orderId=123&decision=true" -H 'Content-Type: application/json' +``` + + + +The output of the request contains the output of the workflow based on the `decision` parameter that we sent. + +```bash +{"approved":true} +``` + +In the application output you should see, that the workflow instance completed correctly: + +```sh +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 2de2b968-900a-4f5b-9092-b26aefbfc6b3 continue +io.dapr.workflows.WorkflowContext : approval-event arrived +i.d.s.e.w.s.PerformTaskActivity : Starting Activity: io.dapr.springboot.examples.wfp.suspendresume.PerformTaskActivity +i.d.s.e.w.s.PerformTaskActivity : Running activity... +i.d.s.e.w.s.PerformTaskActivity : Completing activity... +``` ## Testing workflow executions From fa1a21446bcf7a67c266094479bb1c6601ce3b70 Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 6 Jun 2025 13:18:33 +0100 Subject: [PATCH 03/10] updating README Signed-off-by: salaboy --- spring-boot-examples/workflows/README.md | 25 ++---------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md index 160ec9663..66c95ab1d 100644 --- a/spring-boot-examples/workflows/README.md +++ b/spring-boot-examples/workflows/README.md @@ -431,29 +431,7 @@ io.dapr.workflows.WorkflowContext : Waiting for approval... You should see the Workflow ID that was created, in this example you don't need to remember this id, as you can use the orderId to find the right instance. -When you are ready to approve the order you can send the following request: - - - -To send the event you can run: - -```sh -curl -X POST "localhost:8080/wfp/suspendresume-suspend?orderId=123" -H 'Content-Type: application/json' -``` - - - -Let's suspend the workflow instance by sending the following request: -To send the event you can run: +Let's suspend the workflow instance by sending the following request: ```sh curl -X POST "localhost:8080/wfp/suspendresume-suspend?orderId=123" -H 'Content-Type: application/json' @@ -475,6 +453,7 @@ curl -X POST "localhost:8080/wfp/suspendresume-suspend?orderId=123" -H 'Content- + You should see the output of the requests: ```sh From ef1d0565240222ee2d01aab6dceba79789787722 Mon Sep 17 00:00:00 2001 From: salaboy Date: Fri, 6 Jun 2025 17:28:21 +0100 Subject: [PATCH 04/10] Update README.md Signed-off-by: salaboy --- spring-boot-examples/workflows/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md index 66c95ab1d..0bf7ac6dd 100644 --- a/spring-boot-examples/workflows/README.md +++ b/spring-boot-examples/workflows/README.md @@ -395,11 +395,11 @@ io.dapr.workflows.WorkflowContext : Workflow finished with result: 60 ### Suspend/Resume Workflow example -In this example we start a workflow that executes and activity and then wait for an event. While the workflow instance -is waiting for the event we execute a suspend workflow operation. Once we check the state of the instance a resume +In this example, we start a workflow that executes an activity and then waits for an event. While the workflow instance +is waiting for the event, we execute a suspend workflow operation. Once we check the state of the instance, a resume operation is executed. -To start the workflow you can run: +To start the workflow, you can run: @@ -477,7 +477,7 @@ timeout_seconds: 10 To send the event you can run: ```sh -curl -X POST "localhost:8080/wfp/suspendresume-resume?orderId=123" -H 'Content-Type: application/json' +curl -X POST "localhost:8080/wfp/suspendresume/resume?orderId=123" -H 'Content-Type: application/json' ``` @@ -506,7 +506,7 @@ timeout_seconds: 10 To send the event you can run: ```sh -curl -X POST "localhost:8080/wfp/suspendresume-continue?orderId=123&decision=true" -H 'Content-Type: application/json' +curl -X POST "localhost:8080/wfp/suspendresume/continue?orderId=123&decision=true" -H 'Content-Type: application/json' ``` From 71d51c066e59e4dbcb519e1dcdcd9ed0e3e52548 Mon Sep 17 00:00:00 2001 From: salaboy Date: Tue, 10 Jun 2025 10:13:25 +0800 Subject: [PATCH 08/10] adding output validation Signed-off-by: salaboy --- .../java/io/dapr/examples/workflows/README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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 d0749e1fb..07d34555f 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -662,6 +662,23 @@ For testing the suspend and resume operations we will use the same workflow defi Start the workflow and client using the following commands: + + + ```sh dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.suspendresume.DemoSuspendResumeWorker ``` @@ -670,6 +687,8 @@ dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.suspendresume.DemoSuspendResumeClient ``` + + 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 From ee89be970c890fd541721bd78cf0fd62e61dce04 Mon Sep 17 00:00:00 2001 From: salaboy Date: Tue, 10 Jun 2025 14:34:21 +0800 Subject: [PATCH 09/10] adding missing port Signed-off-by: salaboy --- examples/src/main/java/io/dapr/examples/workflows/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 07d34555f..8328e1253 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -664,7 +664,7 @@ Start the workflow and client using the following commands: ```sh -dapr run --app-id demoworkflowworker --resources-path ./components/workflows -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.suspendresume.DemoSuspendResumeWorker +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 From 646268da91b2114cc747ac3b978ecd8b2b4966e7 Mon Sep 17 00:00:00 2001 From: salaboy Date: Tue, 10 Jun 2025 14:53:51 +0800 Subject: [PATCH 10/10] fixing check conditions Signed-off-by: salaboy --- examples/src/main/java/io/dapr/examples/workflows/README.md | 6 +++++- 1 file changed, 5 insertions(+), 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 8328e1253..b90726080 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -668,8 +668,12 @@ name: Run Suspend/Resume workflow match_order: none output_match_mode: substring expected_stdout_lines: - - "Starting Workflow: io.dapr.examples.workflows.suspendresume.DemoExternalEventWorkflow" - "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..."