diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 42a07c878..efeb39391 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -216,4 +216,10 @@ jobs: run: | mm.py README.md env: - DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} + DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} + - name: Validate Spring Boot Workflow examples + working-directory: ./spring-boot-examples/workflows + run: | + mm.py README.md + env: + DOCKER_HOST: ${{steps.setup_docker.outputs.sock}} diff --git a/spring-boot-examples/pom.xml b/spring-boot-examples/pom.xml index 84ebb6c51..0b333b703 100644 --- a/spring-boot-examples/pom.xml +++ b/spring-boot-examples/pom.xml @@ -21,6 +21,7 @@ producer-app consumer-app + workflows diff --git a/spring-boot-examples/workflows/README.md b/spring-boot-examples/workflows/README.md new file mode 100644 index 000000000..a2004b2db --- /dev/null +++ b/spring-boot-examples/workflows/README.md @@ -0,0 +1,403 @@ +# Dapr Spring Boot Workflow Examples + +This application allows you to run different workflow patterns including: +- Chained Activities +- Parent/Child Workflows +- Continue workflow by sending External Events +- Fan Out/In activities for parallel execution + +## Running these examples from source code + +To run these examples you will need: +- Java SDK +- Maven +- Docker or a container runtime such as Podman + +From the `spring-boot-examples/workflows` directory you can start the service using the test configuration that uses +[Testcontainers](https://testcontainers.com) to boostrap [Dapr](https://dapr.io) by running the following command: + + + + +```sh +../../mvnw spring-boot:test-run +``` + + + +Once the application is running you can trigger the different patterns by sending the following requests: + +### Chaining Activities Workflow example + +The `io.dapr.springboot.examples.wfp.chain.ChainWorkflow` executes three chained activities. For this example the +`ToUpperCaseActivity.java` is used to transform to upper case three strings from an array. + +```mermaid +graph LR + SW((Start + Workflow)) + A1[Activity1] + A2[Activity2] + A3[Activity3] + EW((End + Workflow)) + SW --> A1 + A1 --> A2 + A2 --> A3 + A3 --> EW +``` + + + + +To start the workflow with the three chained activities you can run: + +```sh +curl -X POST localhost:8080/wfp/chain -H 'Content-Type: application/json' +``` + + + + +As result from executing the request you should see: + +```bash +TOKYO, LONDON, SEATTLE +``` + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.chain.ChainWorkflow +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 7625b4af-8c04-408a-93dc-bad753466e43 started +i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Tokyo +i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: TOKYO +i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: London +i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: LONDON +i.d.s.e.wfp.chain.ToUpperCaseActivity : Starting Activity: io.dapr.springboot.examples.wfp.chain.ToUpperCaseActivity +i.d.s.e.wfp.chain.ToUpperCaseActivity : Message Received from input: Seattle +i.d.s.e.wfp.chain.ToUpperCaseActivity : Sending message to output: SEATTLE +io.dapr.workflows.WorkflowContext : Workflow finished with result: TOKYO, LONDON, SEATTLE +``` + +### Parent / Child Workflows example + +In this example we start a Parent workflow that calls a child workflow that execute one activity that reverse the . + +The Parent workflow looks like this: + +```mermaid +graph LR + SW((Start + Workflow)) + subgraph for each word in the input + GWL[Call child workflow] + end + EW((End + Workflow)) + SW --> GWL + GWL --> EW +``` + +The Child workflow looks like this: + +```mermaid +graph LR + SW((Start + Workflow)) + A1[Activity1] + EW((End + Workflow)) + SW --> A1 + A1 --> EW +``` + +To start the parent workflow you can run: + + + + +To start the workflow with the three chained activities you can run: + +```sh +curl -X POST localhost:8080/wfp/child -H 'Content-Type: application/json' +``` + + + + +As result from executing the request you should see: + +```bash +!wolfkroW rpaD olleH +``` + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.child.ParentWorkflow +io.dapr.workflows.WorkflowContext : calling childworkflow with input: Hello Dapr Workflow! +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance f3ec9566-a0fc-4d28-8912-3f3ded3cd8a9 started +io.dapr.workflows.WorkflowContext : Starting ChildWorkflow: io.dapr.springboot.examples.wfp.child.ChildWorkflow +io.dapr.workflows.WorkflowContext : ChildWorkflow received input: Hello Dapr Workflow! +io.dapr.workflows.WorkflowContext : ChildWorkflow is calling Activity: io.dapr.springboot.examples.wfp.child.ReverseActivity +i.d.s.e.wfp.child.ReverseActivity : Starting Activity: io.dapr.springboot.examples.wfp.child.ReverseActivity +i.d.s.e.wfp.child.ReverseActivity : Message Received from input: Hello Dapr Workflow! +i.d.s.e.wfp.child.ReverseActivity : Sending message to output: !wolfkroW rpaD olleH +io.dapr.workflows.WorkflowContext : ChildWorkflow finished with: !wolfkroW rpaD olleH +io.dapr.workflows.WorkflowContext : childworkflow finished with: !wolfkroW rpaD olleH +``` + +### ContinueAsNew Workflows example + +In this example we start a workflow that every 3 seconds schedule a new workflow consistently. This workflow executes +one activity called CleanUpActivity that takes 2 seconds to complete. This loops repeat consistently for 5 times. + +To start the workflow you can run: + + + + +To start the workflow you can run: + +```sh +curl -X POST localhost:8080/wfp/continueasnew -H 'Content-Type: application/json' +``` + + + +As result from executing the request you should see: + +```bash +{"cleanUpTimes":5} +```` + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance b808e7d6-ab47-4eba-8188-dc9ff8780764 started +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:45 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:50 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:48:55 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:49:0 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : Let's do more cleaning. +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow +io.dapr.workflows.WorkflowContext : call CleanUpActivity to do the clean up +i.d.s.e.w.continueasnew.CleanUpActivity : Starting Activity: io.dapr.springboot.examples.wfp.continueasnew.CleanUpActivity +i.d.s.e.w.continueasnew.CleanUpActivity : start clean up work, it may take few seconds to finish... Time:10:49:5 +io.dapr.workflows.WorkflowContext : CleanUpActivity finished +io.dapr.workflows.WorkflowContext : wait 5 seconds for next clean up +io.dapr.workflows.WorkflowContext : We did enough cleaning +``` + +### External Event Workflow example + +In this example we start a workflow that as part of its execution waits for an external event to continue. To correlate +workflows and events we use the parameter `orderId` + +To start the workflow you can run: + + + + +To start the workflow you can run: + +```sh +curl -X POST "localhost:8080/wfp/externalevent?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.externalevent.ExternalEventWorkflow +io.dapr.workflows.WorkflowContext : Waiting for approval... +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance 8a55cf6d-9059-49b1-8c83-fbe17567a02e started +``` + +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/externalevent-continue?orderId=123&decision=true" -H 'Content-Type: application/json' +``` + + + +```bash +{"approved":true} +``` + +In the application output you should see the workflow activities being executed. + +```bash +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance e86bc464-6166-434d-8c91-d99040d6f54e continue +io.dapr.workflows.WorkflowContext : approval granted - do the approved action +i.d.s.e.w.externalevent.ApproveActivity : Starting Activity: io.dapr.springboot.examples.wfp.externalevent.ApproveActivity +i.d.s.e.w.externalevent.ApproveActivity : Running approval activity... +io.dapr.workflows.WorkflowContext : approval-activity finished +``` + +### Fan Out/In Workflow example + +In this example we start a workflow that takes an ArrayList of strings and calls one activity per item in the ArrayList. The activities +are executed and the workflow waits for all of them to complete to aggregate the results. + +```mermaid +graph LR + SW((Start + Workflow)) + subgraph for each word in the input + GWL[GetWordLength] + end + ALL[Wait until all tasks + are completed] + EW((End + Workflow)) + SW --> GWL + GWL --> ALL + ALL --> EW +``` + +To start the workflow you can run: + + + + +To start the workflow you can run: + +```sh +curl -X POST localhost:8080/wfp/fanoutin -H 'Content-Type: application/json' -d @body.json +``` + + + +As result from executing the request you should see: + +```bash +{"wordCount":60} +``` + +In the application output you should see the workflow activities being executed. + +```bash +io.dapr.workflows.WorkflowContext : Starting Workflow: io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow +i.d.s.e.w.WorkflowPatternsRestController : Workflow instance a771a7ba-f9fb-4399-aaee-a2fb0b102e5d started +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 2. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 11. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 17. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 21. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +i.d.s.e.wfp.fanoutin.CountWordsActivity : Starting Activity: io.dapr.springboot.examples.wfp.fanoutin.CountWordsActivity +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity returned: 9. +i.d.s.e.wfp.fanoutin.CountWordsActivity : Activity finished +io.dapr.workflows.WorkflowContext : Workflow finished with result: 60 +``` + + +## Testing workflow executions + +Workflow execution can be tested using Testcontainers and you can find all the tests for the patterns covered in this +application [here](test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java). \ No newline at end of file diff --git a/spring-boot-examples/workflows/body.json b/spring-boot-examples/workflows/body.json new file mode 100644 index 000000000..55471e370 --- /dev/null +++ b/spring-boot-examples/workflows/body.json @@ -0,0 +1,5 @@ +["Hello, world!", + "The quick brown fox jumps over the lazy dog.", + "If a tree falls in the forest and there is no one there to hear it, does it make a sound?", + "The greatest glory in living lies not in never falling, but in rising every time we fall.", + "Always remember that you are absolutely unique. Just like everyone else."] \ No newline at end of file diff --git a/spring-boot-examples/workflows/pom.xml b/spring-boot-examples/workflows/pom.xml new file mode 100644 index 000000000..4711b799b --- /dev/null +++ b/spring-boot-examples/workflows/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + io.dapr + spring-boot-examples + 0.16.0-SNAPSHOT + + + workflows + workflows + Spring Boot, Testcontainers and Dapr Integration Examples :: Workflows + + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + + + io.dapr.spring + dapr-spring-boot-starter + + + io.dapr.spring + dapr-spring-boot-starter-test + test + + + io.rest-assured + rest-assured + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-site-plugin + + true + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + true + + + + + diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java new file mode 100644 index 000000000..378295a2f --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsApplication.java @@ -0,0 +1,27 @@ +/* + * 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.springboot.examples.wfp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class WorkflowPatternsApplication { + + public static void main(String[] args) { + SpringApplication.run(WorkflowPatternsApplication.class, args); + } + +} 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 new file mode 100644 index 000000000..cf1998907 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/WorkflowPatternsRestController.java @@ -0,0 +1,140 @@ +/* + * 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.springboot.examples.wfp; + +import io.dapr.spring.workflows.config.EnableDaprWorkflows; +import io.dapr.springboot.examples.wfp.chain.ChainWorkflow; +import io.dapr.springboot.examples.wfp.child.ParentWorkflow; +import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; +import io.dapr.springboot.examples.wfp.continueasnew.ContinueAsNewWorkflow; +import io.dapr.springboot.examples.wfp.externalevent.Decision; +import io.dapr.springboot.examples.wfp.externalevent.ExternalEventWorkflow; +import io.dapr.springboot.examples.wfp.fanoutin.FanOutInWorkflow; +import io.dapr.springboot.examples.wfp.fanoutin.Result; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.WorkflowInstanceStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; + +@RestController +@EnableDaprWorkflows +public class WorkflowPatternsRestController { + + private final Logger logger = LoggerFactory.getLogger(WorkflowPatternsRestController.class); + + @Autowired + private DaprWorkflowClient daprWorkflowClient; + + private Map ordersToApprove = new HashMap<>(); + + @Bean + public CleanUpLog cleanUpLog(){ + return new CleanUpLog(); + } + + /** + * Run Chain Demo Workflow + * @return the output of the ChainWorkflow execution + */ + @PostMapping("wfp/chain") + public String chain() throws TimeoutException { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ChainWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + return daprWorkflowClient + .waitForInstanceCompletion(instanceId, Duration.ofSeconds(2), true) + .readOutputAs(String.class); + } + + + /** + * Run Child Demo Workflow + * @return confirmation that the workflow instance was created for the workflow pattern child + */ + @PostMapping("wfp/child") + public String child() throws TimeoutException { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ParentWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + return daprWorkflowClient + .waitForInstanceCompletion(instanceId, Duration.ofSeconds(2), true) + .readOutputAs(String.class); + } + + + /** + * Run Fan Out/in Demo Workflow + * @return confirmation that the workflow instance was created for the workflow pattern faninout + */ + @PostMapping("wfp/fanoutin") + public Result faninout(@RequestBody List listOfStrings) throws TimeoutException { + + String instanceId = daprWorkflowClient.scheduleNewWorkflow(FanOutInWorkflow.class, listOfStrings); + logger.info("Workflow instance " + instanceId + " started"); + + // Block until the orchestration completes. Then print the final status, which includes the output. + WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient.waitForInstanceCompletion( + instanceId, + Duration.ofSeconds(30), + true); + logger.info("workflow instance with ID: %s completed with result: %s%n", instanceId, + workflowInstanceStatus.readOutputAs(Result.class)); + return workflowInstanceStatus.readOutputAs(Result.class); + } + + /** + * Run External Event Workflow Pattern + * @return confirmation that the workflow instance was created for the workflow pattern externalevent + */ + @PostMapping("wfp/externalevent") + public String externalevent(@RequestParam("orderId") String orderId) { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ExternalEventWorkflow.class); + ordersToApprove.put(orderId, instanceId); + logger.info("Workflow instance " + instanceId + " started"); + return instanceId; + } + + @PostMapping("wfp/externalevent-continue") + public Decision externaleventContinue(@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); + } + + @PostMapping("wfp/continueasnew") + public CleanUpLog continueasnew() + throws TimeoutException { + String instanceId = daprWorkflowClient.scheduleNewWorkflow(ContinueAsNewWorkflow.class); + logger.info("Workflow instance " + instanceId + " started"); + + WorkflowInstanceStatus workflowInstanceStatus = daprWorkflowClient.waitForInstanceCompletion(instanceId, null, true); + System.out.printf("workflow instance with ID: %s completed.", instanceId); + return workflowInstanceStatus.readOutputAs(CleanUpLog.class); + } + +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java new file mode 100644 index 000000000..d1967247f --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ChainWorkflow.java @@ -0,0 +1,36 @@ +/* + * 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.chain; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ChainWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + String result = ""; + result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "Tokyo", String.class).await() + ", "; + result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "London", String.class).await() + ", "; + result += ctx.callActivity(ToUpperCaseActivity.class.getName(), "Seattle", String.class).await(); + + ctx.getLogger().info("Workflow finished with result: " + result); + ctx.complete(result); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java new file mode 100644 index 000000000..6280bcf71 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/chain/ToUpperCaseActivity.java @@ -0,0 +1,38 @@ +/* + * 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.chain; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ToUpperCaseActivity implements WorkflowActivity { + + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(ToUpperCaseActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + var message = ctx.getInput(String.class); + var newMessage = message.toUpperCase(); + + logger.info("Message Received from input: " + message); + logger.info("Sending message to output: " + newMessage); + + return newMessage; + } +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java new file mode 100644 index 000000000..3267337b1 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ChildWorkflow.java @@ -0,0 +1,37 @@ +/* + * 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.child; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ChildWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting ChildWorkflow: " + ctx.getName()); + + var childWorkflowInput = ctx.getInput(String.class); + ctx.getLogger().info("ChildWorkflow received input: " + childWorkflowInput); + + ctx.getLogger().info("ChildWorkflow is calling Activity: " + ReverseActivity.class.getName()); + String result = ctx.callActivity(ReverseActivity.class.getName(), childWorkflowInput, String.class).await(); + + ctx.getLogger().info("ChildWorkflow finished with: " + result); + ctx.complete(result); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java new file mode 100644 index 000000000..d5d58d3c9 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ParentWorkflow.java @@ -0,0 +1,37 @@ +/* + * 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.child; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ParentWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + var childWorkflowInput = "Hello Dapr Workflow!"; + ctx.getLogger().info("calling childworkflow with input: " + childWorkflowInput); + + var childWorkflowOutput = + ctx.callChildWorkflow(ChildWorkflow.class.getName(), childWorkflowInput, String.class).await(); + + ctx.getLogger().info("childworkflow finished with: " + childWorkflowOutput); + ctx.complete(childWorkflowOutput); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java new file mode 100644 index 000000000..af483cdfb --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/child/ReverseActivity.java @@ -0,0 +1,37 @@ +/* + * 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.child; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ReverseActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(ReverseActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + var message = ctx.getInput(String.class); + var newMessage = new StringBuilder(message).reverse().toString(); + + logger.info("Message Received from input: " + message); + logger.info("Sending message to output: " + newMessage); + + return newMessage; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java new file mode 100644 index 000000000..cb9c7f7aa --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpActivity.java @@ -0,0 +1,52 @@ +/* + * 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.continueasnew; + +import io.dapr.workflows.WorkflowActivity; +import io.dapr.workflows.WorkflowActivityContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +@Component +public class CleanUpActivity implements WorkflowActivity { + + @Autowired + private CleanUpLog cleanUpLog; + + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(CleanUpActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + LocalDateTime now = LocalDateTime.now(); + String cleanUpTimeString = now.getHour() + ":" + now.getMinute() + ":" + now.getSecond(); + logger.info("start clean up work, it may take few seconds to finish... Time:" + cleanUpTimeString); + + //Sleeping for 2 seconds to simulate long running operation + try { + TimeUnit.SECONDS.sleep(2); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + cleanUpLog.increment(); + + return "clean up finish."; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java new file mode 100644 index 000000000..9eb7c3fe9 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/CleanUpLog.java @@ -0,0 +1,15 @@ +package io.dapr.springboot.examples.wfp.continueasnew; + +public class CleanUpLog { + private Integer cleanUpTimes = 0; + + public CleanUpLog() { + } + public void increment() { + this.cleanUpTimes += 1; + } + + public Integer getCleanUpTimes() { + return cleanUpTimes; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java new file mode 100644 index 000000000..ad05ec4c3 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/continueasnew/ContinueAsNewWorkflow.java @@ -0,0 +1,58 @@ +/* + * 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.continueasnew; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class ContinueAsNewWorkflow implements Workflow { + /* + Compared with a CRON schedule, this periodic workflow example will never overlap. + For example, a CRON schedule that executes a cleanup every hour will execute it at 1:00, 2:00, 3:00 etc. + and could potentially run into overlap issues if the cleanup takes longer than an hour. + In this example, however, if the cleanup takes 30 minutes, and we create a timer for 1 hour between cleanups, + then it will be scheduled at 1:00, 2:30, 4:00, etc. and there is no chance of overlap. + */ + + @Autowired + private CleanUpLog cleanUpLog; + + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + ctx.getLogger().info("call CleanUpActivity to do the clean up"); + ctx.callActivity(CleanUpActivity.class.getName(), cleanUpLog).await(); + ctx.getLogger().info("CleanUpActivity finished"); + + ctx.getLogger().info("wait 5 seconds for next clean up"); + ctx.createTimer(Duration.ofSeconds(3)).await(); + + if(cleanUpLog.getCleanUpTimes() < 5) { + // continue the workflow. + ctx.getLogger().info("Let's do more cleaning."); + ctx.continueAsNew(null); + } else{ + ctx.getLogger().info("We did enough cleaning"); + ctx.complete(cleanUpLog); + } + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java new file mode 100644 index 000000000..e8ea8f7c8 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ApproveActivity.java @@ -0,0 +1,41 @@ +/* + * 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.externalevent; + +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 ApproveActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(ApproveActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + logger.info("Running approval activity..."); + //Sleeping for 5 seconds to simulate long running operation + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return new Decision(true); + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java new file mode 100644 index 000000000..2f8d2fee9 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/Decision.java @@ -0,0 +1,33 @@ +/* + * 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.springboot.examples.wfp.externalevent; + +public class Decision { + private Boolean approved; + + public Decision() { + } + + public Decision(Boolean approved) { + this.approved = approved; + } + + public Boolean getApproved() { + return approved; + } + + public void setApproved(Boolean approved) { + this.approved = approved; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java new file mode 100644 index 000000000..bd4079376 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/DenyActivity.java @@ -0,0 +1,41 @@ +/* + * 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.externalevent; + +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 DenyActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(DenyActivity.class); + logger.info("Starting Activity: " + ctx.getName()); + + logger.info("Running denied activity..."); + //Sleeping for 5 seconds to simulate long running operation + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + return new Decision(false); + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.java new file mode 100644 index 000000000..952d5eede --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/externalevent/ExternalEventWorkflow.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.externalevent; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +@Component +public class ExternalEventWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + ctx.getLogger().info("Waiting for approval..."); + Boolean approved = ctx.waitForExternalEvent("Approval", boolean.class).await(); + Decision decision = null; + if (approved) { + ctx.getLogger().info("approval granted - do the approved action"); + decision = ctx.callActivity(ApproveActivity.class.getName(), Decision.class).await(); + + ctx.getLogger().info("approval-activity finished"); + } else { + ctx.getLogger().info("approval denied - send a notification"); + decision = ctx.callActivity(DenyActivity.class.getName(), Decision.class).await(); + ctx.getLogger().info("denied-activity finished"); + } + ctx.complete(decision); + }; + } +} diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.java new file mode 100644 index 000000000..5863d5692 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/CountWordsActivity.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.fanoutin; + +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.StringTokenizer; + +@Component +public class CountWordsActivity implements WorkflowActivity { + @Override + public Object run(WorkflowActivityContext ctx) { + Logger logger = LoggerFactory.getLogger(CountWordsActivity.class); + logger.info("Starting Activity: {}", ctx.getName()); + + String input = ctx.getInput(String.class); + StringTokenizer tokenizer = new StringTokenizer(input); + int result = tokenizer.countTokens(); + + logger.info("Activity returned: {}.", result); + logger.info("Activity finished"); + + return result; + } +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java new file mode 100644 index 000000000..09f3edc57 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/FanOutInWorkflow.java @@ -0,0 +1,51 @@ +/* + * 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.fanoutin; + +import io.dapr.durabletask.Task; +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class FanOutInWorkflow implements Workflow { + @Override + public WorkflowStub create() { + return ctx -> { + + ctx.getLogger().info("Starting Workflow: " + ctx.getName()); + + + // The input is a list of objects that need to be operated on. + // In this example, inputs are expected to be strings. + List inputs = ctx.getInput(List.class); + + // Fan-out to multiple concurrent activity invocations, each of which does a word count. + List> tasks = inputs.stream() + .map(input -> ctx.callActivity(CountWordsActivity.class.getName(), input.toString(), Integer.class)) + .collect(Collectors.toList()); + + // Fan-in to get the total word count from all the individual activity results. + List allWordCountResults = ctx.allOf(tasks).await(); + int totalWordCount = allWordCountResults.stream().mapToInt(Integer::intValue).sum(); + + ctx.getLogger().info("Workflow finished with result: " + totalWordCount); + // Save the final result as the orchestration output. + ctx.complete(new Result(totalWordCount)); + }; + } +} \ No newline at end of file diff --git a/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java new file mode 100644 index 000000000..015484014 --- /dev/null +++ b/spring-boot-examples/workflows/src/main/java/io/dapr/springboot/examples/wfp/fanoutin/Result.java @@ -0,0 +1,33 @@ +/* + * 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.springboot.examples.wfp.fanoutin; + +public class Result { + private Integer wordCount; + + public Result() { + } + + public Result(Integer wordCount) { + this.wordCount = wordCount; + } + + public Integer getWordCount() { + return wordCount; + } + + public void setWordCount(Integer wordCount) { + this.wordCount = wordCount; + } +}; diff --git a/spring-boot-examples/workflows/src/main/resources/application.properties b/spring-boot-examples/workflows/src/main/resources/application.properties new file mode 100644 index 000000000..7fd4ccd5b --- /dev/null +++ b/spring-boot-examples/workflows/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=workflow-patterns-app diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java new file mode 100644 index 000000000..a0e3a087c --- /dev/null +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java @@ -0,0 +1,43 @@ +/* + * 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.springboot.examples.wfp; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; + +import java.util.Collections; + +import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; + +@TestConfiguration(proxyBeanMethods = false) +public class DaprTestContainersConfig { + + @Bean + @ServiceConnection + public DaprContainer daprContainer() { + + return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("workflow-patterns-app") + .withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true)))) + .withAppPort(8080) + .withAppHealthCheckPath("/actuator/health") + .withAppChannelAddress("host.testcontainers.internal"); + } + + + +} diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java new file mode 100644 index 000000000..8459f8d50 --- /dev/null +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/TestWorkflowPatternsApplication.java @@ -0,0 +1,31 @@ +/* + * 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.springboot.examples.wfp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class TestWorkflowPatternsApplication { + + public static void main(String[] args) { + + SpringApplication.from(WorkflowPatternsApplication::main) + .with(DaprTestContainersConfig.class) + .run(args); + org.testcontainers.Testcontainers.exposeHostPorts(8080); + } + +} 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 new file mode 100644 index 000000000..4ca36cb58 --- /dev/null +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java @@ -0,0 +1,142 @@ +/* + * 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.springboot.examples.wfp; + +import io.dapr.client.DaprClient; +import io.dapr.springboot.DaprAutoConfiguration; +import io.dapr.springboot.examples.wfp.continueasnew.CleanUpLog; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Arrays; +import java.util.List; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringBootTest(classes = {TestWorkflowPatternsApplication.class, DaprTestContainersConfig.class, + DaprAutoConfiguration.class, }, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +class WorkflowPatternsAppTests { + + @Autowired + private DaprClient daprClient; + + @BeforeEach + void setUp() { + RestAssured.baseURI = "http://localhost:" + 8080; + org.testcontainers.Testcontainers.exposeHostPorts(8080); + } + + + @Test + void testChainWorkflow() { + given().contentType(ContentType.JSON) + .body("") + .when() + .post("/wfp/chain") + .then() + .statusCode(200).body(containsString("TOKYO, LONDON, SEATTLE")); + } + + @Test + void testChildWorkflow() { + given().contentType(ContentType.JSON) + .body("") + .when() + .post("/wfp/child") + .then() + .statusCode(200).body(containsString("!wolfkroW rpaD olleH")); + } + + @Test + void testFanOutIn() { + List listOfStrings = Arrays.asList( + "Hello, world!", + "The quick brown fox jumps over the lazy dog.", + "If a tree falls in the forest and there is no one there to hear it, does it make a sound?", + "The greatest glory in living lies not in never falling, but in rising every time we fall.", + "Always remember that you are absolutely unique. Just like everyone else."); + + given().contentType(ContentType.JSON) + .body(listOfStrings) + .when() + .post("/wfp/fanoutin") + .then() + .statusCode(200).body("wordCount",equalTo(60)); + } + + @Test + void testExternalEventApprove() { + + given() + .queryParam("orderId", "123") + .when() + .post("/wfp/externalevent") + .then() + .statusCode(200).extract().asString(); + + + + given() + .queryParam("orderId", "123") + .queryParam("decision", true) + .when() + .post("/wfp/externalevent-continue") + .then() + .statusCode(200).body("approved", equalTo(true)); + } + + @Test + void testExternalEventDeny() { + + given() + .queryParam("orderId", "123") + .when() + .post("/wfp/externalevent") + .then() + .statusCode(200).extract().asString(); + + + + given() + .queryParam("orderId", "123") + .queryParam("decision", false) + .when() + .post("/wfp/externalevent-continue") + .then() + .statusCode(200).body("approved", equalTo(false)); + } + + + @Test + void testContinueAsNew() { + //This call blocks until all the clean up activities are executed + CleanUpLog cleanUpLog = given().contentType(ContentType.JSON) + .body("") + .when() + .post("/wfp/continueasnew") + .then() + .statusCode(200).extract().as(CleanUpLog.class); + + assertEquals(5, cleanUpLog.getCleanUpTimes()); + } + +} diff --git a/spring-boot-examples/workflows/src/test/resources/application.properties b/spring-boot-examples/workflows/src/test/resources/application.properties new file mode 100644 index 000000000..20d5fc037 --- /dev/null +++ b/spring-boot-examples/workflows/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=workflow-patterns-app \ No newline at end of file