Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
distribution: 'temurin'
java-version: ${{ env.JDK_VER }}
- name: Run tests
run: ./mvnw clean install -B -q
run: ./mvnw clean install -B -q -DskipITs=true
- name: Codecov
uses: codecov/[email protected]
- name: Upload test report for sdk
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ jobs:
mm.py README.md
env:
DOCKER_HOST: ${{steps.setup_docker.outputs.sock}}
- name: Validate Spring Boot Workflow examples
working-directory: ./spring-boot-examples/workflows
- name: Validate Spring Boot Workflow Patterns examples
working-directory: ./spring-boot-examples/workflows/patterns
run: |
mm.py README.md
env:
Expand Down
3 changes: 2 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<spotbugs.fail>true</spotbugs.fail>
<spotbugs.exclude.filter.file>../spotbugs-exclude.xml</spotbugs.exclude.filter.file>
<argLine>--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED</argLine>
<failsafe.version>3.2.2</failsafe.version>
<failsafe.version>3.5.3</failsafe.version>
<surefire.version>3.2.2</surefire.version>
<junit-bom.version>5.11.4</junit-bom.version>
<snakeyaml.version>2.0</snakeyaml.version>
Expand Down Expand Up @@ -665,6 +665,7 @@
<id>integration-tests</id>
<modules>
<module>sdk-tests</module>
<module>spring-boot-examples</module>
</modules>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
@SpringBootTest(classes = {TestConsumerApplication.class, DaprTestContainersConfig.class,
ConsumerAppTestConfiguration.class, DaprAutoConfiguration.class},
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ConsumerAppTests {
class ConsumerAppIT {

private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";

Expand Down
17 changes: 17 additions & 0 deletions spring-boot-examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<classesDirectory>${project.build.outputDirectory}</classesDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<configuration>
<excludes>
<!-- Exclude full package from test coverage -->
<exclude>**/*io/dapr/springboot/examples/**</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
DaprAutoConfiguration.class, CustomerWorkflow.class, CustomerFollowupActivity.class,
RegisterCustomerActivity.class, CustomerStore.class},
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
class ProducerAppTests {
class ProducerAppIT {

private static final String SUBSCRIPTION_MESSAGE_PATTERN = ".*app is subscribed to the following topics.*";

Expand Down
91 changes: 91 additions & 0 deletions spring-boot-examples/workflows/multi-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Multi App workflow Example

This example demonstrates how you can create distributed workflows where the orchestrator doesn't host the workflow activities.

For more documentation about how Multi App Workflows work [check the official documentation here](https://v1-16.docs.dapr.io/developing-applications/building-blocks/workflow/workflow-multi-app/).

This example is composed by three Spring Boot applications:
- `orchestrator`: The `orchestrator` app contains the Dapr Workflow definition and expose REST endpoints to create and raise events against workflow instances.
- `worker-one`: The `worker-one` app contains the `RegisterCustomerActivity` definition, which will be called by the `orchestrator` app.
- `worker-two`: The `worker-two` app contains the `CustomerFollowupActivity` definition, which will be called by the `orchestrator` app.

To start the applications you need to run the following commands on separate terminals, starting from the `multi-app` directory.
To start the `orchestrator` app run:
```bash
cd orchestrator/
mvn -Dspring-boot.run.arguments="--reuse=true" clean spring-boot:test-run
```

The `orchestrator` application will run on port `8080`.

On a separate terminal, to start the `worker-one` app run:
```bash
cd worker-one/
mvn -Dspring-boot.run.arguments="--reuse=true" clean spring-boot:test-run
```

The `worker-one` application will run on port `8081`.

On a separate terminal, to start the `worker-two` app run:
```bash
cd worker-two/
mvn -Dspring-boot.run.arguments="--reuse=true" clean spring-boot:test-run
```

The `worker-two` application will run on port `8082`.

You can create new workflow instances of the `CustomerWorkflow` by calling the `/customers` endpoint of the `orchestrator` application.

```bash
curl -X POST localhost:8080/customers -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```

The workflow definition [`CustomerWorkflow`](orchstrator/src/main/java/io/dapr/springboot/examples/orchestrator/CustomerWorkflow.java) that you can find inside the `orchestrator` app,
performs the following orchestration when a new workflow instance is created:

- Call the `RegisterCustomerActivity` activity which can be found inside the `worker-one` application.
- You can find in the workflow definition the configuration to make reference to an Activity that is hosted by a different Dapr application.
```java
customer = ctx.callActivity("io.dapr.springboot.examples.workerone.RegisterCustomerActivity",
customer,
new WorkflowTaskOptions("worker-one"),
Customer.class).
await();
```
- Wait for an external event of type `CustomerReachOut` with a timeout of 5 minutes:
```java
ctx.waitForExternalEvent("CustomerReachOut", Duration.ofMinutes(5), Customer.class).await();
```
- You can check the status of the workflow for a given customer by sending the following request:
```shell
curl -X POST localhost:8080/customers/status -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```
- You can call the following endpoint on the `orchestrator` app to raise the external event:
```shell
curl -X POST localhost:8080/customers/followup -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```
- When the event is received, the workflow move forward to the last activity called `CustomerFollowUpActivity`, that can be found on the `worker-two` app.
```java
customer = ctx.callActivity("io.dapr.springboot.examples.workertwo.CustomerFollowupActivity",
customer,
new WorkflowTaskOptions("worker-two"),
Customer.class).
await();
```
- The workflow completes by handing out the final version of the `Customer` object that has been modified the workflow activities. You can retrieve the `Customer` payload
by running the following command:
```shell
curl -X POST localhost:8080/customers/output -H 'Content-Type: application/json' -d '{ "customerName": "salaboy" }'
```

## Testing Multi App Workflows

Testing becomes a complex task when you are dealing with multiple Spring Boot applications. For testing this workflow,
we rely on [Testcontainers](https://testcontainers.com) to create the entire setup which enable us to run the workflow end to end.

You can find the end-to-end test in the [`OrchestratorAppIT.java`](orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/OrchestratorAppIT.java) class inside the `orchestrator` application.
This test interact with the application REST endpoints to validate their correct execution.

But the magic behind the test can be located in the [`DaprTestContainersConfig.class`](orchestrator/src/test/java/io/dapr/springboot/examples/orchestrator/DaprTestContainersConfig.java) which defines the configuration for
all the Dapr containers and the `worker-one` and `worker-two` applications. Check this class to gain a deeper understand how to configure
multiple Dapr-enabled applications.
81 changes: 81 additions & 0 deletions spring-boot-examples/workflows/multi-app/orchestrator/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?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>io.dapr</groupId>
<artifactId>multi-app</artifactId>
<version>1.17.0-SNAPSHOT</version>
</parent>

<artifactId>orchestrator</artifactId>
<name>orchestrator</name>
<description>Spring Boot, Testcontainers and Dapr Integration Examples :: Orchestrator App</description>

<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>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.redis</groupId>
<artifactId>testcontainers-redis</artifactId>
<version>2.2.2</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>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<configuration>
<!-- Skip checkstyle for auto-generated code -->
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2025 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/

package io.dapr.springboot.examples.orchestrator;

public class Customer {
private String customerName;
private String workflowId;
private boolean inCustomerDB = false;
private boolean followUp = false;

public boolean isFollowUp() {
return followUp;
}

public void setFollowUp(boolean followUp) {
this.followUp = followUp;
}

public boolean isInCustomerDB() {
return inCustomerDB;
}

public void setInCustomerDB(boolean inCustomerDB) {
this.inCustomerDB = inCustomerDB;
}

public String getWorkflowId() {
return workflowId;
}

public void setWorkflowId(String workflowId) {
this.workflowId = workflowId;
}

public String getCustomerName() {
return customerName;
}

public void setCustomerName(String customerName) {
this.customerName = customerName;
}

@Override
public String toString() {
return "Customer [customerName=" + customerName + ", workflowId=" + workflowId + ", inCustomerDB="
+ inCustomerDB + ", followUp=" + followUp + "]";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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.orchestrator;

import io.dapr.durabletask.TaskCanceledException;
import io.dapr.durabletask.TaskFailedException;
import io.dapr.workflows.Workflow;
import io.dapr.workflows.WorkflowStub;
import io.dapr.workflows.WorkflowTaskOptions;
import org.springframework.stereotype.Component;

import java.time.Duration;

@Component
public class CustomerWorkflow implements Workflow {

@Override
public WorkflowStub create() {
return ctx -> {
String instanceId = ctx.getInstanceId();
Customer customer = ctx.getInput(Customer.class);
customer.setWorkflowId(instanceId);
ctx.getLogger().info("Let's register the customer: {}", customer.getCustomerName());

customer = ctx.callActivity("io.dapr.springboot.examples.workerone.RegisterCustomerActivity", customer,
new WorkflowTaskOptions("worker-one"), Customer.class).await();

ctx.getLogger().info("Let's wait for the customer: {} to request a follow up.", customer.getCustomerName());
ctx.waitForExternalEvent("CustomerReachOut", Duration.ofMinutes(5), Customer.class).await();

ctx.getLogger().info("Let's book a follow up for the customer: {}", customer.getCustomerName());
customer = ctx.callActivity("io.dapr.springboot.examples.workertwo.CustomerFollowupActivity",
customer, new WorkflowTaskOptions("worker-two"), Customer.class).await();

ctx.getLogger().info("Congratulations the customer: {} is happy!", customer.getCustomerName());

ctx.getLogger().info("Final customer: {} ", customer);
ctx.complete(customer);
};
}
}
Loading
Loading