Skip to content

Commit 1331b60

Browse files
Feat Cross App CallActivity (dapr#1468)
* cross app ex Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * update protoc cmd Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * feedback Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * builder pattern Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * fix protoc Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * debug log levels for test containers Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * update readme and add debugging info Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * add IT test for cross app call activity Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * cleanup test Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * sysout -> ctx.logger Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * reset pom Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * rm debug lines from readme Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * fix header + rm customports Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * use consts Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * rm waitfor call Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * rm pubsub Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * rm timeout Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * reset empty lines added Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * reset appname for daprcontainer Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * reset empty line diff Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * rm constructor info from readme Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * debug -> info Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * rm super.start Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * reset dapr container diff Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * add test for codecov Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * up timeout time to unblock PR Signed-off-by: Cassandra Coyle <cassie@diagrid.io> * deps: Update durabletask-client to 1.5.10 Signed-off-by: Javier Aliaga <javier@diagrid.io> * ci: Revert build timeout Signed-off-by: Javier Aliaga <javier@diagrid.io> * review: Use ctx.getLogger Signed-off-by: Javier Aliaga <javier@diagrid.io> * chore: Fix review comments Signed-off-by: Javier Aliaga <javier@diagrid.io> * chore: more review comments fixes Signed-off-by: Javier Aliaga <javier@diagrid.io> * test: Use testcontainers in CrossApp IT test Signed-off-by: Javier Aliaga <javier@diagrid.io> * chore: Load classpath for IT with all dependencies Signed-off-by: Javier Aliaga <javier@diagrid.io> --------- Signed-off-by: Cassandra Coyle <cassie@diagrid.io> Signed-off-by: Javier Aliaga <javier@diagrid.io> Co-authored-by: Javier Aliaga <javier@diagrid.io>
1 parent c915a4a commit 1331b60

File tree

22 files changed

+1076
-7
lines changed

22 files changed

+1076
-7
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ jobs:
145145
run: ./mvnw clean install -B -q -DskipTests
146146
- name: Integration tests using spring boot version ${{ matrix.spring-boot-version }}
147147
id: integration_tests
148-
run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -Pintegration-tests verify
148+
run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -Pintegration-tests dependency:copy-dependencies verify
149149
env:
150150
DOCKER_HOST: ${{steps.setup_docker.outputs.sock}}
151151
- name: Upload failsafe test report for sdk-tests on failure

examples/src/main/java/io/dapr/examples/workflows/README.md

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Those examples contain the following workflow patterns:
5353
4. [External Event Pattern](#external-event-pattern)
5454
5. [Child-workflow Pattern](#child-workflow-pattern)
5555
6. [Compensation Pattern](#compensation-pattern)
56+
7. [Cross-App Pattern](#cross-app-pattern)
5657

5758
### Chaining Pattern
5859
In the chaining pattern, a sequence of activities executes in a specific order.
@@ -681,6 +682,158 @@ Key Points:
681682
4. Each activity simulates work with a short delay for demonstration purposes
682683

683684

685+
### Cross-App Pattern
686+
687+
The cross-app pattern allows workflows to call activities that are hosted in different Dapr applications. This is useful for microservices architectures allowing multiple applications to host activities that can be orchestrated by Dapr Workflows.
688+
689+
The `CrossAppWorkflow` class defines the workflow. It demonstrates calling activities in different apps using the `appId` parameter in `WorkflowTaskOptions`. See the code snippet below:
690+
```java
691+
public class CrossAppWorkflow implements Workflow {
692+
@Override
693+
public WorkflowStub create() {
694+
return ctx -> {
695+
var logger = ctx.getLogger();
696+
logger.info("=== WORKFLOW STARTING ===");
697+
logger.info("Starting CrossAppWorkflow: {}", ctx.getName());
698+
logger.info("Workflow name: {}", ctx.getName());
699+
logger.info("Workflow instance ID: {}", ctx.getInstanceId());
700+
701+
String input = ctx.getInput(String.class);
702+
logger.info("CrossAppWorkflow received input: {}", input);
703+
logger.info("Workflow input: {}", input);
704+
705+
// Call an activity in another app by passing in an active appID to the WorkflowTaskOptions
706+
logger.info("Calling cross-app activity in 'app2'...");
707+
logger.info("About to call cross-app activity in app2...");
708+
String crossAppResult = ctx.callActivity(
709+
App2TransformActivity.class.getName(),
710+
input,
711+
new WorkflowTaskOptions("app2"),
712+
String.class
713+
).await();
714+
715+
// Call another activity in a different app
716+
logger.info("Calling cross-app activity in 'app3'...");
717+
logger.info("About to call cross-app activity in app3...");
718+
String finalResult = ctx.callActivity(
719+
App3FinalizeActivity.class.getName(),
720+
crossAppResult,
721+
new WorkflowTaskOptions("app3"),
722+
String.class
723+
).await();
724+
logger.info("Final cross-app activity result: {}", finalResult);
725+
logger.info("Final cross-app activity result: {}", finalResult);
726+
727+
logger.info("CrossAppWorkflow finished with: {}", finalResult);
728+
logger.info("=== WORKFLOW COMPLETING WITH: {} ===" , finalResult);
729+
ctx.complete(finalResult);
730+
};
731+
}
732+
}
733+
734+
```
735+
736+
The `App2TransformActivity` class defines an activity in app2 that transforms the input string. See the code snippet below:
737+
```java
738+
public class App2TransformActivity implements WorkflowActivity {
739+
@Override
740+
public Object run(WorkflowActivityContext ctx) {
741+
var logger = ctx.getLogger();
742+
logger.info("=== App2: TransformActivity called ===");
743+
String input = ctx.getInput(String.class);
744+
logger.info("Input: {}", input);
745+
746+
// Transform the input
747+
String result = input.toUpperCase() + " [TRANSFORMED BY APP2]";
748+
749+
logger.info("Output: {}", result);
750+
return result;
751+
}
752+
}
753+
```
754+
755+
The `App3FinalizeActivity` class defines an activity in app3 that finalizes the processing. See the code snippet below:
756+
```java
757+
public class App3FinalizeActivity implements WorkflowActivity {
758+
@Override
759+
public Object run(WorkflowActivityContext ctx) {
760+
var logger = ctx.getLogger();
761+
logger.info("=== App3: FinalizeActivity called ===");
762+
String input = ctx.getInput(String.class);
763+
logger.info("Input: ", input);
764+
765+
// Finalize the processing
766+
String result = input + " [FINALIZED BY APP3]";
767+
768+
logger.info("Output: {}", result);
769+
return result;
770+
}
771+
}
772+
```
773+
774+
**Key Features:**
775+
- **Cross-app activity calls**: Call activities in different Dapr applications specifying the appID in the WorkflowTaskOptions
776+
- **WorkflowTaskOptions with appId**: Specify which app should handle the activity
777+
- **Combined with retry policies**: Use app ID along with retry policies and handlers
778+
- **Error handling**: Works the same as local activity calls
779+
780+
**Requirements:**
781+
- Multiple Dapr applications running with different app IDs
782+
- Activities registered in the target applications
783+
- Proper Dapr workflow runtime configuration
784+
785+
**Important Limitations:**
786+
- **Cross-app calls are currently supported for activities only**
787+
- **Child workflow cross-app calls (suborchestration) are NOT supported**
788+
- The app ID must match the Dapr application ID of the target service
789+
790+
**Running the Cross-App Example:**
791+
792+
This example requires running multiple Dapr applications simultaneously. You'll need to run the following commands in separate terminals:
793+
794+
1. **Start the main workflow worker (crossapp-worker):**
795+
```sh
796+
dapr run --app-id crossapp-worker --resources-path ./components/workflows --dapr-grpc-port 50001 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.crossapp.CrossAppWorker
797+
```
798+
799+
2. **Start app2 worker (handles App2TransformActivity):**
800+
```sh
801+
dapr run --app-id app2 --resources-path ./components/workflows --dapr-grpc-port 50002 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.crossapp.App2Worker
802+
```
803+
804+
3. **Start app3 worker (handles App3FinalizeActivity):**
805+
```sh
806+
dapr run --app-id app3 --resources-path ./components/workflows --dapr-grpc-port 50003 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.crossapp.App3Worker
807+
```
808+
809+
4. **Run the workflow client:**
810+
```sh
811+
java -Djava.util.logging.ConsoleHandler.level=FINE -Dio.dapr.durabletask.level=FINE -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.crossapp.CrossAppWorkflowClient "Hello World"
812+
```
813+
814+
**Expected Output:**
815+
816+
The client will show:
817+
```text
818+
=== Starting Cross-App Workflow Client ===
819+
Input: Hello World
820+
Created DaprWorkflowClient successfully
821+
Attempting to start new workflow...
822+
Started a new cross-app workflow with instance ID: 001113f3-b9d9-438c-932a-a9a9b70b9460
823+
Waiting for workflow completion...
824+
Workflow instance with ID: 001113f3-b9d9-438c-932a-a9a9b70b9460 completed with result: HELLO WORLD [TRANSFORMED BY APP2] [FINALIZED BY APP3]
825+
```
826+
827+
The workflow demonstrates:
828+
1. The workflow starts in the main worker (crossapp-worker)
829+
2. Calls an activity in 'app2' using cross-app functionality
830+
3. Calls an activity in 'app3' using cross-app functionality
831+
4. The workflow completes with the final result from all activities
832+
833+
This pattern is particularly useful for:
834+
- Microservices architectures where activities are distributed across multiple services
835+
- Multi-tenant applications where activities are isolated by app ID
836+
684837
### Suspend/Resume Pattern
685838

686839
Workflow instances can be suspended and resumed. This example shows how to use the suspend and resume commands.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.crossapp;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
19+
/**
20+
* TransformActivity for App2 - transforms input to uppercase.
21+
* This activity is called cross-app from the main workflow.
22+
*/
23+
public class App2TransformActivity implements WorkflowActivity {
24+
@Override
25+
public Object run(WorkflowActivityContext context) {
26+
String input = context.getInput(String.class);
27+
var logger = context.getLogger();
28+
logger.info("=== App2: TransformActivity called ===");
29+
logger.info("Input: {}", input);
30+
String result = input.toUpperCase() + " [TRANSFORMED BY APP2]";
31+
logger.info("Output: {}", result);
32+
return result;
33+
}
34+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.crossapp;
15+
16+
import io.dapr.workflows.runtime.WorkflowRuntime;
17+
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
18+
19+
/**
20+
* App2 Worker - registers only the TransformActivity.
21+
* This app will handle cross-app activity calls from the main workflow.
22+
*/
23+
public class App2Worker {
24+
25+
public static void main(String[] args) throws Exception {
26+
System.out.println("=== Starting App2Worker ===");
27+
// Register the Workflow with the builder
28+
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
29+
.registerActivity(App2TransformActivity.class);
30+
31+
// Build and start the workflow runtime
32+
try (WorkflowRuntime runtime = builder.build()) {
33+
System.out.println("App2 is ready to receive cross-app activity calls...");
34+
runtime.start();
35+
}
36+
}
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.crossapp;
15+
16+
import io.dapr.workflows.WorkflowActivity;
17+
import io.dapr.workflows.WorkflowActivityContext;
18+
19+
/**
20+
* FinalizeActivity for App3 - adds final processing.
21+
* This activity is called cross-app from the main workflow.
22+
*/
23+
public class App3FinalizeActivity implements WorkflowActivity {
24+
@Override
25+
public Object run(WorkflowActivityContext context) {
26+
String input = context.getInput(String.class);
27+
var logger = context.getLogger();
28+
logger.info("=== App3: FinalizeActivity called ===");
29+
logger.info("Input: {}", input);
30+
String result = input + " [FINALIZED BY APP3]";
31+
logger.info("Output: {}", result);
32+
return result;
33+
}
34+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.crossapp;
15+
16+
import io.dapr.workflows.runtime.WorkflowRuntime;
17+
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
18+
19+
/**
20+
* App3 Worker - registers only the FinalizeActivity.
21+
* This app will handle cross-app activity calls from the main workflow.
22+
*/
23+
public class App3Worker {
24+
25+
public static void main(String[] args) throws Exception {
26+
System.out.println("=== Starting App3Worker ===");
27+
// Register the Workflow with the builder
28+
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
29+
.registerActivity(App3FinalizeActivity.class);
30+
31+
// Build and start the workflow runtime
32+
try (WorkflowRuntime runtime = builder.build()) {
33+
System.out.println("App3 is ready to receive cross-app activity calls...");
34+
runtime.start();
35+
}
36+
}
37+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2025 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.examples.workflows.crossapp;
15+
16+
import io.dapr.workflows.runtime.WorkflowRuntime;
17+
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
18+
19+
public class CrossAppWorker {
20+
21+
public static void main(String[] args) throws Exception {
22+
// Register the Workflow with the builder
23+
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
24+
.registerWorkflow(CrossAppWorkflow.class);
25+
26+
// Build and start the workflow runtime
27+
try (WorkflowRuntime runtime = builder.build()) {
28+
System.out.println("CrossAppWorker started - registered CrossAppWorkflow only");
29+
runtime.start();
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)