adding child workflow example

Signed-off-by: salaboy <Salaboy@gmail.com>
This commit is contained in:
salaboy 2025-06-18 10:11:46 +09:00
parent 9a182685a8
commit 3291320cf9
15 changed files with 611 additions and 0 deletions

View File

@ -0,0 +1,94 @@
# Child Workflows
This tutorial demonstrates how a workflow can call child workflows that are part of the same application. Child workflows can be used to break up large workflows into smaller, reusable parts. For more information about child workflows see the [Dapr docs](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-features-concepts/#child-workflows).
## Inspect the code
Open the [`ParentWorkflow.java`](src/main/java/io/dapr/springboot/examples/child/ParentWorkflow.java) file. This file contains the definition for the workflow.
The parent workflow iterates over the input array and schedules an instance of the `ChildWorkflow` for each of the input elements. The `ChildWorkflow` is a basic task-chaining workflow that contains a sequence of two activities.
When all of the instances of the `ChildWorkflow` complete, then the parent workflow finishes.
### Parent workflow
```mermaid
graph LR
SW((Start
Workflow))
subgraph for each word in the input
GWL[Call child workflow]
end
ALL[Wait until all tasks
are completed]
EW((End
Workflow))
SW --> GWL
GWL --> ALL
ALL --> EW
```
### Child workflow
```mermaid
graph LR
SW((Start
Workflow))
A1[Activity1]
A2[Activity2]
EW((End
Workflow))
SW --> A1
A1 --> A2
A2 --> EW
```
## Run the tutorial
1. Use a terminal to navigate to the `tutorials/workflow/java/child-workflows` folder.
2. Build and run the project using Maven.
```bash
mvn spring-boot:test-run
```
3. Use the POST request in the [`childworkflows.http`](./childworkflows.http) file to start the workflow, or use this cURL command:
```bash
curl -i --request POST \
--url http://localhost:8080/start \
--header 'content-type: application/json' \
--data '["Item 1","Item 2"]'
```
The input of the workflow is an array with two strings:
```json
[
"Item 1",
"Item 2"
]
```
The app logs should show both the items in the input values array being processed by each activity in the child workflow as follows:
```text
io.dapr.springboot.examples.child.Activity1 : Received input: Item 1.
io.dapr.springboot.examples.child.Activity2 : Received input: Item 1 is processed.
io.dapr.springboot.examples.child.Activity1 : Received input: Item 2.
io.dapr.springboot.examples.child.Activity2 : Received input: Item 2 is processed.
```
4. Use the GET request in the [`childworkflows.http`](./childworkflows.http) file to get the status of the workflow, or use this cURL command:
```bash
curl --request GET --url http://localhost:8080/output
```
5. The expected serialized output of the workflow is an array with two strings:
```txt
"["Item 1 is processed as a child workflow.","Item 2 is processed as a child workflow."]"
```
6. Stop the application by pressing `Ctrl+C`.

View File

@ -0,0 +1,14 @@
@apphost=http://localhost:8080
### Start the ParentWorkflow workflow
# @name startWorkflowRequest
POST {{ apphost }}/start
Content-Type: application/json
[
"Item 1",
"Item 2"
]
### Get the workflow status
GET {{ apphost }}/output

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<artifactId>child-workflows</artifactId>
<name>child-workflows</name>
<description>Child Workflows Example</description>
<properties>
<dapr.spring.version>0.15.0-rc-7</dapr.spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter</artifactId>
<version>${dapr.spring.version}</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<version>${dapr.spring.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ChildWorkflowsApplication {
public static void main(String[] args) {
SpringApplication.run(ChildWorkflowsApplication.class, args);
}
}

View File

@ -0,0 +1,34 @@
/*
* 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;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ChildWorkflowsConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder().build();
}
@Bean
public ObjectMapper mapper() {
return new ObjectMapper();
}
}

View File

@ -0,0 +1,75 @@
/*
* 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;
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
import io.dapr.springboot.examples.child.ChildWorkflow;
import io.dapr.springboot.examples.child.ParentWorkflow;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
@RestController
@EnableDaprWorkflows
public class ChildWorkflowsRestController {
private final Logger logger = LoggerFactory.getLogger(ChildWorkflowsRestController.class);
@Autowired
private DaprWorkflowClient daprWorkflowClient;
/*
* **Note:** This local variable is used for examples purposes only.
* For production scenarios, you will need to map workflowInstanceIds to your business scenarios.
*/
private String instanceId;
/**
* Run Parent/Child Demo Workflow
*
* @return the instanceId of the ParentWorkflow execution
*/
@PostMapping("start")
public String child(@RequestBody List<String> input) throws TimeoutException {
instanceId = daprWorkflowClient.scheduleNewWorkflow(ParentWorkflow.class, input);
return instanceId;
}
/**
* Obtain the output of the workflow
*
* @return the output of the ChainWorkflow execution
*/
@GetMapping("output")
public List<String> output() throws TimeoutException {
WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(instanceId, true);
if (instanceState != null) {
return instanceState.readOutputAs(List.class);
}
return Collections.singletonList("N/A");
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.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 Activity1 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Activity1.class);
var input = ctx.getInput(String.class);
logger.info("{} : Received input: {}.", ctx.getName(), input);
return input + " is processed";
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.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 Activity2 implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
Logger logger = LoggerFactory.getLogger(Activity2.class);
var input = ctx.getInput(String.class);
logger.info("{} : Received input: {}.", ctx.getName(), input);
return input + " as a child workflow.";
}
}

View File

@ -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.child;
import io.dapr.durabletask.Task;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowStub;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class ChildWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: {}", ctx.getName());
var input = ctx.getInput(String.class);
String result1 = ctx.callActivity(Activity1.class.getName(), input, String.class).await();
String childWorkflowResults = ctx.callActivity(Activity2.class.getName(), result1, String.class).await();
ctx.complete(childWorkflowResults);
};
}
}

View File

@ -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.child;
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 ParentWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
ctx.getLogger().info("Starting Workflow: {}", ctx.getName());
List<String> inputs = ctx.getInput(List.class);
List<Task<String>> tasks = inputs.stream()
.map(input -> ctx.callChildWorkflow(ChildWorkflow.class.getName(), input, String.class))
.collect(Collectors.toList());
// Fan-in to get the total word count from all the individual activity results.
List<String> allChildWorkflowsResults = ctx.allOf(tasks).await();
ctx.complete(allChildWorkflowsResults);
};
}
}

View File

@ -0,0 +1 @@
spring.application.name=child-workflows

View File

@ -0,0 +1,85 @@
/*
* 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;
import io.dapr.client.DaprClient;
import io.dapr.springboot.DaprAutoConfiguration;
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 org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import static io.dapr.springboot.examples.StringMatchesUUIDPattern.matchesThePatternOfAUUID;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(classes = {TestChildWorkflowsApplication.class, DaprTestContainersConfig.class,
DaprAutoConfiguration.class, },
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ChildWorkflowsAppTests {
@Autowired
private DaprClient daprClient;
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + 8080;
org.testcontainers.Testcontainers.exposeHostPorts(8080);
}
@Test
void testChildWorkflows() {
given().contentType(ContentType.JSON)
.body("[\"Item 1\",\"Item 2\"]")
.when()
.post("/start")
.then()
.statusCode(200).body(matchesThePatternOfAUUID());
String output = given().contentType(ContentType.JSON)
.when()
.get("/output")
.then()
.statusCode(200).extract().asString();
assertEquals("[\"Item 1 is processed as a child workflow.\",\"Item 2 is processed as a child workflow.\"]", output);
}
}
class StringMatchesUUIDPattern extends TypeSafeMatcher<String> {
private static final String UUID_REGEX = "[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}";
@Override
protected boolean matchesSafely(String s) {
return s.matches(UUID_REGEX);
}
@Override
public void describeTo(Description description) {
description.appendText("a string matching the pattern of a UUID");
}
public static Matcher<String> matchesThePatternOfAUUID() {
return new StringMatchesUUIDPattern();
}
}

View File

@ -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;
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");
}
}

View File

@ -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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestChildWorkflowsApplication {
public static void main(String[] args) {
SpringApplication.from(ChildWorkflowsApplication::main)
.with(DaprTestContainersConfig.class)
.run(args);
org.testcontainers.Testcontainers.exposeHostPorts(8080);
}
}

View File

@ -0,0 +1 @@
spring.application.name=child-workflows