Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8f453d6
cross app ex
cicoyle Jul 25, 2025
455467c
update protoc cmd
cicoyle Jul 25, 2025
869cb83
feedback
cicoyle Jul 25, 2025
613dfb0
builder pattern
cicoyle Aug 6, 2025
c927075
fix protoc
cicoyle Aug 7, 2025
466e799
debug log levels for test containers
cicoyle Aug 7, 2025
7411384
update readme and add debugging info
cicoyle Aug 11, 2025
2876fee
add IT test for cross app call activity
cicoyle Aug 12, 2025
4836553
cleanup test
cicoyle Aug 12, 2025
8a8b36e
sysout -> ctx.logger
cicoyle Aug 13, 2025
af2e57d
reset pom
cicoyle Aug 20, 2025
fcaf699
rm debug lines from readme
cicoyle Aug 20, 2025
b5f541b
fix header + rm customports
cicoyle Aug 21, 2025
710ba9d
use consts
cicoyle Aug 21, 2025
0d5c425
rm waitfor call
cicoyle Aug 21, 2025
b0e5e23
rm pubsub
cicoyle Aug 21, 2025
015d200
rm timeout
cicoyle Aug 21, 2025
46f6c3b
reset empty lines added
cicoyle Aug 21, 2025
a45cb00
reset appname for daprcontainer
cicoyle Aug 21, 2025
14c9185
reset empty line diff
cicoyle Aug 21, 2025
6f42fed
rm constructor info from readme
cicoyle Aug 21, 2025
5dd363d
debug -> info
cicoyle Aug 21, 2025
3613cd3
rm super.start
cicoyle Aug 22, 2025
1aaefa6
reset dapr container diff
cicoyle Aug 22, 2025
4e9fab4
add test for codecov
cicoyle Aug 26, 2025
c1aadc1
up timeout time to unblock PR
cicoyle Aug 26, 2025
973ed3c
deps: Update durabletask-client to 1.5.10
javier-aliaga Sep 1, 2025
a5ecd04
ci: Revert build timeout
javier-aliaga Sep 1, 2025
ed8214d
review: Use ctx.getLogger
javier-aliaga Sep 1, 2025
b756cde
chore: Fix review comments
javier-aliaga Sep 2, 2025
efa979c
chore: more review comments fixes
javier-aliaga Sep 2, 2025
101904d
test: Use testcontainers in CrossApp IT test
javier-aliaga Sep 2, 2025
13921bf
chore: Load classpath for IT with all dependencies
javier-aliaga Sep 2, 2025
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 @@ -145,7 +145,7 @@ jobs:
run: ./mvnw clean install -B -q -DskipTests
- name: Integration tests using spring boot version ${{ matrix.spring-boot-version }}
id: integration_tests
run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -Pintegration-tests verify
run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -Pintegration-tests dependency:copy-dependencies verify
env:
DOCKER_HOST: ${{steps.setup_docker.outputs.sock}}
- name: Upload failsafe test report for sdk-tests on failure
Expand Down
153 changes: 153 additions & 0 deletions examples/src/main/java/io/dapr/examples/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Those examples contain the following workflow patterns:
4. [External Event Pattern](#external-event-pattern)
5. [Child-workflow Pattern](#child-workflow-pattern)
6. [Compensation Pattern](#compensation-pattern)
7. [Cross-App Pattern](#cross-app-pattern)

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


### Cross-App Pattern

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.

The `CrossAppWorkflow` class defines the workflow. It demonstrates calling activities in different apps using the `appId` parameter in `WorkflowTaskOptions`. See the code snippet below:
```java
public class CrossAppWorkflow implements Workflow {
@Override
public WorkflowStub create() {
return ctx -> {
var logger = ctx.getLogger();
logger.info("=== WORKFLOW STARTING ===");
logger.info("Starting CrossAppWorkflow: {}", ctx.getName());
logger.info("Workflow name: {}", ctx.getName());
logger.info("Workflow instance ID: {}", ctx.getInstanceId());

String input = ctx.getInput(String.class);
logger.info("CrossAppWorkflow received input: {}", input);
logger.info("Workflow input: {}", input);

// Call an activity in another app by passing in an active appID to the WorkflowTaskOptions
logger.info("Calling cross-app activity in 'app2'...");
logger.info("About to call cross-app activity in app2...");
String crossAppResult = ctx.callActivity(
App2TransformActivity.class.getName(),
input,
new WorkflowTaskOptions("app2"),
String.class
).await();

// Call another activity in a different app
logger.info("Calling cross-app activity in 'app3'...");
logger.info("About to call cross-app activity in app3...");
String finalResult = ctx.callActivity(
App3FinalizeActivity.class.getName(),
crossAppResult,
new WorkflowTaskOptions("app3"),
String.class
).await();
logger.info("Final cross-app activity result: {}", finalResult);
logger.info("Final cross-app activity result: {}", finalResult);

logger.info("CrossAppWorkflow finished with: {}", finalResult);
logger.info("=== WORKFLOW COMPLETING WITH: {} ===" , finalResult);
ctx.complete(finalResult);
};
}
}

```

The `App2TransformActivity` class defines an activity in app2 that transforms the input string. See the code snippet below:
```java
public class App2TransformActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
var logger = ctx.getLogger();
logger.info("=== App2: TransformActivity called ===");
String input = ctx.getInput(String.class);
logger.info("Input: {}", input);

// Transform the input
String result = input.toUpperCase() + " [TRANSFORMED BY APP2]";

logger.info("Output: {}", result);
return result;
}
}
```

The `App3FinalizeActivity` class defines an activity in app3 that finalizes the processing. See the code snippet below:
```java
public class App3FinalizeActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
var logger = ctx.getLogger();
logger.info("=== App3: FinalizeActivity called ===");
String input = ctx.getInput(String.class);
logger.info("Input: ", input);

// Finalize the processing
String result = input + " [FINALIZED BY APP3]";

logger.info("Output: {}", result);
return result;
}
}
```

**Key Features:**
- **Cross-app activity calls**: Call activities in different Dapr applications specifying the appID in the WorkflowTaskOptions
- **WorkflowTaskOptions with appId**: Specify which app should handle the activity
- **Combined with retry policies**: Use app ID along with retry policies and handlers
- **Error handling**: Works the same as local activity calls

**Requirements:**
- Multiple Dapr applications running with different app IDs
- Activities registered in the target applications
- Proper Dapr workflow runtime configuration

**Important Limitations:**
- **Cross-app calls are currently supported for activities only**
- **Child workflow cross-app calls (suborchestration) are NOT supported**
- The app ID must match the Dapr application ID of the target service

**Running the Cross-App Example:**

This example requires running multiple Dapr applications simultaneously. You'll need to run the following commands in separate terminals:

1. **Start the main workflow worker (crossapp-worker):**
```sh
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
```

2. **Start app2 worker (handles App2TransformActivity):**
```sh
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
```

3. **Start app3 worker (handles App3FinalizeActivity):**
```sh
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
```

4. **Run the workflow client:**
```sh
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"
```

**Expected Output:**

The client will show:
```text
=== Starting Cross-App Workflow Client ===
Input: Hello World
Created DaprWorkflowClient successfully
Attempting to start new workflow...
Started a new cross-app workflow with instance ID: 001113f3-b9d9-438c-932a-a9a9b70b9460
Waiting for workflow completion...
Workflow instance with ID: 001113f3-b9d9-438c-932a-a9a9b70b9460 completed with result: HELLO WORLD [TRANSFORMED BY APP2] [FINALIZED BY APP3]
```

The workflow demonstrates:
1. The workflow starts in the main worker (crossapp-worker)
2. Calls an activity in 'app2' using cross-app functionality
3. Calls an activity in 'app3' using cross-app functionality
4. The workflow completes with the final result from all activities

This pattern is particularly useful for:
- Microservices architectures where activities are distributed across multiple services
- Multi-tenant applications where activities are isolated by app ID

### Suspend/Resume Pattern

Workflow instances can be suspended and resumed. This example shows how to use the suspend and resume commands.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.examples.workflows.crossapp;

import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;

/**
* TransformActivity for App2 - transforms input to uppercase.
* This activity is called cross-app from the main workflow.
*/
public class App2TransformActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext context) {
String input = context.getInput(String.class);
var logger = context.getLogger();
logger.info("=== App2: TransformActivity called ===");
logger.info("Input: {}", input);
String result = input.toUpperCase() + " [TRANSFORMED BY APP2]";
logger.info("Output: {}", result);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.examples.workflows.crossapp;

import io.dapr.workflows.runtime.WorkflowRuntime;
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;

/**
* App2 Worker - registers only the TransformActivity.
* This app will handle cross-app activity calls from the main workflow.
*/
public class App2Worker {

public static void main(String[] args) throws Exception {
System.out.println("=== Starting App2Worker ===");
// Register the Workflow with the builder
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
.registerActivity(App2TransformActivity.class);

// Build and start the workflow runtime
try (WorkflowRuntime runtime = builder.build()) {
System.out.println("App2 is ready to receive cross-app activity calls...");
runtime.start();
}
}
}
Original file line number Diff line number Diff line change
@@ -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.examples.workflows.crossapp;

import io.dapr.workflows.WorkflowActivity;
import io.dapr.workflows.WorkflowActivityContext;

/**
* FinalizeActivity for App3 - adds final processing.
* This activity is called cross-app from the main workflow.
*/
public class App3FinalizeActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext context) {
String input = context.getInput(String.class);
var logger = context.getLogger();
logger.info("=== App3: FinalizeActivity called ===");
logger.info("Input: {}", input);
String result = input + " [FINALIZED BY APP3]";
logger.info("Output: {}", result);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* 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.examples.workflows.crossapp;

import io.dapr.workflows.runtime.WorkflowRuntime;
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;

/**
* App3 Worker - registers only the FinalizeActivity.
* This app will handle cross-app activity calls from the main workflow.
*/
public class App3Worker {

public static void main(String[] args) throws Exception {
System.out.println("=== Starting App3Worker ===");
// Register the Workflow with the builder
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
.registerActivity(App3FinalizeActivity.class);

// Build and start the workflow runtime
try (WorkflowRuntime runtime = builder.build()) {
System.out.println("App3 is ready to receive cross-app activity calls...");
runtime.start();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.examples.workflows.crossapp;

import io.dapr.workflows.runtime.WorkflowRuntime;
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;

public class CrossAppWorker {

public static void main(String[] args) throws Exception {
// Register the Workflow with the builder
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
.registerWorkflow(CrossAppWorkflow.class);

// Build and start the workflow runtime
try (WorkflowRuntime runtime = builder.build()) {
System.out.println("CrossAppWorker started - registered CrossAppWorkflow only");
runtime.start();
}
}
}
Loading
Loading