mirror of https://github.com/dapr/java-sdk.git
Feat Cross App CallActivity (#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>
This commit is contained in:
parent
a160717c91
commit
9fc16c262a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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.Workflow;
|
||||
import io.dapr.workflows.WorkflowStub;
|
||||
import io.dapr.workflows.WorkflowTaskOptions;
|
||||
|
||||
/**
|
||||
* Example workflow that demonstrates cross-app activity calls.
|
||||
* This workflow calls activities in different apps using the appId parameter.
|
||||
*/
|
||||
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("CrossAppWorkflow finished with: {}", finalResult);
|
||||
logger.info("=== WORKFLOW COMPLETING WITH: {} ===", finalResult);
|
||||
ctx.complete(finalResult);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.client.DaprWorkflowClient;
|
||||
import io.dapr.workflows.client.WorkflowInstanceStatus;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
* Cross-App Workflow Client - starts and monitors workflows.
|
||||
*
|
||||
* 1. Create a workflow client
|
||||
* 2. Start a new workflow instance
|
||||
* 3. Wait for completion and get results
|
||||
*/
|
||||
public class CrossAppWorkflowClient {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 1) {
|
||||
System.out.println("Usage: CrossAppWorkflowClientExample <input>");
|
||||
System.out.println("Example: CrossAppWorkflowClientExample \"Hello World\"");
|
||||
return;
|
||||
}
|
||||
|
||||
String input = args[0];
|
||||
System.out.println("=== Starting Cross-App Workflow Client ===");
|
||||
System.out.println("Input: " + input);
|
||||
|
||||
try (DaprWorkflowClient client = new DaprWorkflowClient()) {
|
||||
System.out.println("Created DaprWorkflowClient successfully");
|
||||
|
||||
// Start a new workflow instance
|
||||
System.out.println("Attempting to start new workflow...");
|
||||
String instanceId = client.scheduleNewWorkflow(CrossAppWorkflow.class, input);
|
||||
System.out.printf("Started a new cross-app workflow with instance ID: %s%n", instanceId);
|
||||
|
||||
// Wait for the workflow to complete
|
||||
System.out.println("Waiting for workflow completion...");
|
||||
WorkflowInstanceStatus workflowInstanceStatus =
|
||||
client.waitForInstanceCompletion(instanceId, null, true);
|
||||
|
||||
// Get the result
|
||||
String result = workflowInstanceStatus.readOutputAs(String.class);
|
||||
System.out.printf("Workflow instance with ID: %s completed with result: %s%n", instanceId, result);
|
||||
|
||||
} catch (TimeoutException | InterruptedException e) {
|
||||
System.err.println("Error waiting for workflow completion: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Error creating workflow client or starting workflow: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.it.testcontainers.workflows.crossapp;
|
||||
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class App2TransformActivity implements WorkflowActivity {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(App2TransformActivity.class);
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
String input = ctx.getInput(String.class);
|
||||
logger.info("=== App2: TransformActivity called ===");
|
||||
logger.info("Input: {}", input);
|
||||
|
||||
String output = input.toUpperCase() + " [TRANSFORMED BY APP2]";
|
||||
logger.info("Output: {}", output);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.it.testcontainers.workflows.crossapp;
|
||||
|
||||
import io.dapr.workflows.runtime.WorkflowRuntime;
|
||||
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
|
||||
|
||||
/**
|
||||
* App2Worker - registers the App2TransformActivity.
|
||||
* 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 (App2TransformActivity) ===");
|
||||
|
||||
// Register the Activity with the builder
|
||||
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
|
||||
.registerActivity(App2TransformActivity.class);
|
||||
|
||||
// Build and start the workflow runtime
|
||||
try (WorkflowRuntime runtime = builder.build()) {
|
||||
System.out.println("App2Worker started - registered App2TransformActivity only");
|
||||
System.out.println("App2 is ready to receive cross-app activity calls...");
|
||||
System.out.println("Waiting for cross-app activity calls...");
|
||||
runtime.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.it.testcontainers.workflows.crossapp;
|
||||
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class App3FinalizeActivity implements WorkflowActivity {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(App3FinalizeActivity.class);
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
String input = ctx.getInput(String.class);
|
||||
logger.info("=== App3: FinalizeActivity called ===");
|
||||
logger.info("Input: {}", input);
|
||||
|
||||
String output = input + " [FINALIZED BY APP3]";
|
||||
logger.info("Output: {}", output);
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.it.testcontainers.workflows.crossapp;
|
||||
|
||||
import io.dapr.workflows.runtime.WorkflowRuntime;
|
||||
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
|
||||
|
||||
/**
|
||||
* App3Worker - registers the App3FinalizeActivity.
|
||||
* 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 (App3FinalizeActivity) ===");
|
||||
|
||||
// Register the Activity with the builder
|
||||
WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder()
|
||||
.registerActivity(App3FinalizeActivity.class);
|
||||
|
||||
// Build and start the workflow runtime
|
||||
try (WorkflowRuntime runtime = builder.build()) {
|
||||
System.out.println("App3Worker started - registered App3FinalizeActivity only");
|
||||
System.out.println("App3 is ready to receive cross-app activity calls...");
|
||||
System.out.println("Waiting for cross-app activity calls...");
|
||||
runtime.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.it.testcontainers.workflows.crossapp;
|
||||
|
||||
import io.dapr.workflows.runtime.WorkflowRuntime;
|
||||
import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
|
||||
|
||||
/**
|
||||
* CrossAppWorker - registers only the CrossAppWorkflow.
|
||||
* This is the main workflow orchestrator that will call activities in other apps.
|
||||
*/
|
||||
public class CrossAppWorker {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("=== Starting CrossAppWorker (Workflow Orchestrator) ===");
|
||||
|
||||
// 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");
|
||||
System.out.println("Waiting for workflow orchestration requests...");
|
||||
runtime.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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.it.testcontainers.workflows.crossapp;
|
||||
|
||||
import io.dapr.workflows.Workflow;
|
||||
import io.dapr.workflows.WorkflowStub;
|
||||
import io.dapr.workflows.WorkflowTaskOptions;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
public class CrossAppWorkflow implements Workflow {
|
||||
@Override
|
||||
public WorkflowStub create() {
|
||||
return ctx -> {
|
||||
Logger logger = ctx.getLogger();
|
||||
String instanceId = ctx.getInstanceId();
|
||||
logger.info("Starting CrossAppWorkflow: {}", ctx.getName());
|
||||
logger.info("Instance ID: {}", instanceId);
|
||||
|
||||
String input = ctx.getInput(String.class);
|
||||
logger.info("Workflow input: {}", input);
|
||||
|
||||
// Call App2TransformActivity in app2
|
||||
logger.info("Calling cross-app activity in 'app2'...");
|
||||
String transformedByApp2 = ctx.callActivity(
|
||||
App2TransformActivity.class.getName(),
|
||||
input,
|
||||
new WorkflowTaskOptions("app2"),
|
||||
String.class
|
||||
).await();
|
||||
|
||||
// Call App3FinalizeActivity in app3
|
||||
logger.info("Calling cross-app activity in 'app3'...");
|
||||
String finalizedByApp3 = ctx.callActivity(
|
||||
App3FinalizeActivity.class.getName(),
|
||||
transformedByApp2,
|
||||
new WorkflowTaskOptions("app3"),
|
||||
String.class
|
||||
).await();
|
||||
|
||||
logger.info("Final cross-app activity result: {}", finalizedByApp3);
|
||||
ctx.complete(finalizedByApp3);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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.it.testcontainers.workflows.crossapp;
|
||||
|
||||
import io.dapr.testcontainers.Component;
|
||||
import io.dapr.testcontainers.DaprContainer;
|
||||
import io.dapr.testcontainers.DaprLogLevel;
|
||||
import io.dapr.testcontainers.DaprPlacementContainer;
|
||||
import io.dapr.testcontainers.DaprSchedulerContainer;
|
||||
import io.dapr.workflows.client.DaprWorkflowClient;
|
||||
import io.dapr.workflows.client.WorkflowInstanceStatus;
|
||||
import io.dapr.workflows.client.WorkflowRuntimeStatus;
|
||||
import io.dapr.config.Properties;
|
||||
import net.bytebuddy.utility.dispatcher.JavaDispatcher;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.Network;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.MountableFile;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.wait.strategy.Wait;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
|
||||
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG;
|
||||
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* Cross-App Pattern integration test.
|
||||
*
|
||||
* This test demonstrates the cross-app pattern by:
|
||||
* 1. Starting 3 Dapr containers (crossapp-worker, app2, app3)
|
||||
* 2. Launching Java processes that register workflows/activities in separate apps
|
||||
* 3. Executing a cross-app workflow
|
||||
* 4. Asserting successful completion
|
||||
*/
|
||||
@Testcontainers
|
||||
@Tag("testcontainers")
|
||||
public class WorkflowsCrossAppCallActivityIT {
|
||||
|
||||
private static final Network DAPR_NETWORK = Network.newNetwork();
|
||||
|
||||
@Container
|
||||
private final static DaprPlacementContainer sharedPlacementContainer = new DaprPlacementContainer(DAPR_PLACEMENT_IMAGE_TAG)
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.withNetworkAliases("placement")
|
||||
.withReuse(false);
|
||||
|
||||
@Container
|
||||
private final static DaprSchedulerContainer sharedSchedulerContainer = new DaprSchedulerContainer(DAPR_SCHEDULER_IMAGE_TAG)
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.withNetworkAliases("scheduler")
|
||||
.withReuse(false);
|
||||
|
||||
// Main workflow orchestrator container
|
||||
@Container
|
||||
private final static DaprContainer MAIN_WORKFLOW_SIDECAR = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("crossapp-worker")
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.withNetworkAliases("main-workflow-sidecar")
|
||||
.withPlacementContainer(sharedPlacementContainer)
|
||||
.withSchedulerContainer(sharedSchedulerContainer)
|
||||
.withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true")))
|
||||
.withDaprLogLevel(DaprLogLevel.DEBUG)
|
||||
.dependsOn(sharedPlacementContainer, sharedSchedulerContainer)
|
||||
.withLogConsumer(outputFrame -> System.out.println("MAIN_WORKFLOW: " + outputFrame.getUtf8String()))
|
||||
.withAppChannelAddress("host.testcontainers.internal");
|
||||
|
||||
// App2 container for App2TransformActivity
|
||||
@Container
|
||||
private final static DaprContainer APP2_SIDECAR = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("app2")
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.withNetworkAliases("app2-sidecar")
|
||||
.withPlacementContainer(sharedPlacementContainer)
|
||||
.withSchedulerContainer(sharedSchedulerContainer)
|
||||
.withAppChannelAddress("main-workflow-sidecar:3500")
|
||||
.withDaprLogLevel(DaprLogLevel.DEBUG)
|
||||
.dependsOn(sharedPlacementContainer, sharedSchedulerContainer, MAIN_WORKFLOW_SIDECAR)
|
||||
.withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true")))
|
||||
.withLogConsumer(outputFrame -> System.out.println("APP2: " + outputFrame.getUtf8String()));
|
||||
|
||||
// App3 container for App3FinalizeActivity
|
||||
@Container
|
||||
private final static DaprContainer APP3_SIDECAR = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("app3")
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.withNetworkAliases("app3-sidecar")
|
||||
.withPlacementContainer(sharedPlacementContainer)
|
||||
.withSchedulerContainer(sharedSchedulerContainer)
|
||||
.withAppChannelAddress("main-workflow-sidecar:3500")
|
||||
.withDaprLogLevel(DaprLogLevel.DEBUG)
|
||||
.dependsOn(sharedPlacementContainer, sharedSchedulerContainer, MAIN_WORKFLOW_SIDECAR)
|
||||
.withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true")))
|
||||
.withLogConsumer(outputFrame -> System.out.println("APP3: " + outputFrame.getUtf8String()));
|
||||
|
||||
|
||||
// TestContainers for each app
|
||||
@Container
|
||||
private static GenericContainer<?> crossappWorker = new GenericContainer<>("openjdk:17-jdk-slim")
|
||||
.withCopyFileToContainer(MountableFile.forHostPath("target"), "/app")
|
||||
.withWorkingDirectory("/app")
|
||||
.withCommand("java", "-cp", "test-classes:classes:dependency/*:*",
|
||||
"-Ddapr.app.id=crossapp-worker",
|
||||
"-Ddapr.grpc.endpoint=main-workflow-sidecar:50001",
|
||||
"-Ddapr.http.endpoint=main-workflow-sidecar:3500",
|
||||
"io.dapr.it.testcontainers.workflows.crossapp.CrossAppWorker")
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.dependsOn(MAIN_WORKFLOW_SIDECAR)
|
||||
.waitingFor(Wait.forLogMessage(".*CrossAppWorker started.*", 1))
|
||||
.withLogConsumer(outputFrame -> System.out.println("CrossAppWorker: " + outputFrame.getUtf8String()));
|
||||
|
||||
@Container
|
||||
private final static GenericContainer<?> app2Worker = new GenericContainer<>("openjdk:17-jdk-slim")
|
||||
.withCopyFileToContainer(MountableFile.forHostPath("target"), "/app")
|
||||
.withWorkingDirectory("/app")
|
||||
.withCommand("java", "-cp", "test-classes:classes:dependency/*:*",
|
||||
"-Ddapr.app.id=app2",
|
||||
"-Ddapr.grpc.endpoint=app2-sidecar:50001",
|
||||
"-Ddapr.http.endpoint=app2-sidecar:3500",
|
||||
"io.dapr.it.testcontainers.workflows.crossapp.App2Worker")
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.dependsOn(APP2_SIDECAR)
|
||||
.waitingFor(Wait.forLogMessage(".*App2Worker started.*", 1))
|
||||
.withLogConsumer(outputFrame -> System.out.println("App2Worker: " + outputFrame.getUtf8String()));
|
||||
|
||||
@Container
|
||||
private final static GenericContainer<?> app3Worker = new GenericContainer<>("openjdk:17-jdk-slim")
|
||||
.withCopyFileToContainer(MountableFile.forHostPath("target"), "/app")
|
||||
.withWorkingDirectory("/app")
|
||||
.withCommand("java", "-cp", "test-classes:classes:dependency/*:*",
|
||||
"-Ddapr.app.id=app3",
|
||||
"-Ddapr.grpc.endpoint=app3-sidecar:50001",
|
||||
"-Ddapr.http.endpoint=app3-sidecar:3500",
|
||||
"io.dapr.it.testcontainers.workflows.crossapp.App3Worker")
|
||||
.withNetwork(DAPR_NETWORK)
|
||||
.dependsOn(APP3_SIDECAR)
|
||||
.waitingFor(Wait.forLogMessage(".*App3Worker started.*", 1))
|
||||
.withLogConsumer(outputFrame -> System.out.println("App3Worker: " + outputFrame.getUtf8String()));
|
||||
|
||||
@Test
|
||||
public void testCrossAppWorkflow() throws Exception {
|
||||
// TestContainers wait strategies ensure all containers are ready before this test runs
|
||||
|
||||
String input = "Hello World";
|
||||
String expectedOutput = "HELLO WORLD [TRANSFORMED BY APP2] [FINALIZED BY APP3]";
|
||||
|
||||
// Create workflow client connected to the main workflow orchestrator
|
||||
// Use the same endpoint configuration that the workers use
|
||||
// The workers use host.testcontainers.internal:50001
|
||||
Map<String, String> propertyOverrides = Map.of(
|
||||
"dapr.grpc.endpoint", MAIN_WORKFLOW_SIDECAR.getGrpcEndpoint(),
|
||||
"dapr.http.endpoint", MAIN_WORKFLOW_SIDECAR.getHttpEndpoint()
|
||||
);
|
||||
|
||||
Properties clientProperties = new Properties(propertyOverrides);
|
||||
DaprWorkflowClient workflowClient = new DaprWorkflowClient(clientProperties);
|
||||
|
||||
try {
|
||||
String instanceId = workflowClient.scheduleNewWorkflow(CrossAppWorkflow.class, input);
|
||||
assertNotNull(instanceId, "Workflow instance ID should not be null");
|
||||
workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false);
|
||||
|
||||
WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, null, true);
|
||||
assertNotNull(workflowStatus, "Workflow status should not be null");
|
||||
assertEquals(WorkflowRuntimeStatus.COMPLETED, workflowStatus.getRuntimeStatus(),
|
||||
"Workflow should complete successfully");
|
||||
String workflowOutput = workflowStatus.readOutputAs(String.class);
|
||||
assertEquals(expectedOutput, workflowOutput, "Workflow output should match expected result");
|
||||
} finally {
|
||||
workflowClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
<dependency>
|
||||
<groupId>io.dapr</groupId>
|
||||
<artifactId>durabletask-client</artifactId>
|
||||
<version>1.5.7</version>
|
||||
<version>1.5.10</version>
|
||||
</dependency>
|
||||
<!--
|
||||
manually declare durabletask-client's jackson dependencies
|
||||
|
|
|
|||
|
|
@ -17,18 +17,48 @@ public class WorkflowTaskOptions {
|
|||
|
||||
private final WorkflowTaskRetryPolicy retryPolicy;
|
||||
private final WorkflowTaskRetryHandler retryHandler;
|
||||
private final String appId;
|
||||
|
||||
public WorkflowTaskOptions(WorkflowTaskRetryPolicy retryPolicy, WorkflowTaskRetryHandler retryHandler) {
|
||||
this.retryPolicy = retryPolicy;
|
||||
this.retryHandler = retryHandler;
|
||||
this(retryPolicy, retryHandler, null);
|
||||
}
|
||||
|
||||
public WorkflowTaskOptions(WorkflowTaskRetryPolicy retryPolicy) {
|
||||
this(retryPolicy, null);
|
||||
this(retryPolicy, null, null);
|
||||
}
|
||||
|
||||
public WorkflowTaskOptions(WorkflowTaskRetryHandler retryHandler) {
|
||||
this(null, retryHandler);
|
||||
this(null, retryHandler, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for WorkflowTaskOptions with app ID for cross-app calls.
|
||||
*
|
||||
* @param appId the ID of the app to call the activity in
|
||||
*/
|
||||
public WorkflowTaskOptions(String appId) {
|
||||
this(null, null, appId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for WorkflowTaskOptions with retry policy, retry handler, and app ID.
|
||||
*
|
||||
* @param retryPolicy the retry policy
|
||||
* @param retryHandler the retry handler
|
||||
* @param appId the app ID for cross-app activity calls
|
||||
*/
|
||||
public WorkflowTaskOptions(WorkflowTaskRetryPolicy retryPolicy, WorkflowTaskRetryHandler retryHandler, String appId) {
|
||||
this.retryPolicy = retryPolicy;
|
||||
this.retryHandler = retryHandler;
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
public WorkflowTaskOptions(WorkflowTaskRetryPolicy retryPolicy, String appId) {
|
||||
this(retryPolicy, null, appId);
|
||||
}
|
||||
|
||||
public WorkflowTaskOptions(WorkflowTaskRetryHandler retryHandler, String appId) {
|
||||
this(null, retryHandler, appId);
|
||||
}
|
||||
|
||||
public WorkflowTaskRetryPolicy getRetryPolicy() {
|
||||
|
|
@ -39,4 +69,8 @@ public class WorkflowTaskOptions {
|
|||
return retryHandler;
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,11 @@ public class DefaultWorkflowContext implements WorkflowContext {
|
|||
RetryPolicy retryPolicy = toRetryPolicy(options.getRetryPolicy());
|
||||
RetryHandler retryHandler = toRetryHandler(options.getRetryHandler());
|
||||
|
||||
return new TaskOptions(retryPolicy, retryHandler);
|
||||
return TaskOptions.builder()
|
||||
.retryPolicy(retryPolicy)
|
||||
.retryHandler(retryHandler)
|
||||
.appID(options.getAppId())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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.workflows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class WorkflowTaskOptionsTest {
|
||||
|
||||
@Test
|
||||
void testConstructorWithRetryPolicyAndHandler() {
|
||||
WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build();
|
||||
WorkflowTaskRetryHandler retryHandler = (context) -> true;
|
||||
|
||||
WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy, retryHandler);
|
||||
|
||||
assertEquals(retryPolicy, options.getRetryPolicy());
|
||||
assertEquals(retryHandler, options.getRetryHandler());
|
||||
assertNull(options.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithRetryPolicyOnly() {
|
||||
WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build();
|
||||
|
||||
WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy);
|
||||
|
||||
assertEquals(retryPolicy, options.getRetryPolicy());
|
||||
assertNull(options.getRetryHandler());
|
||||
assertNull(options.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithRetryHandlerOnly() {
|
||||
WorkflowTaskRetryHandler retryHandler = (context) -> true;
|
||||
|
||||
WorkflowTaskOptions options = new WorkflowTaskOptions(retryHandler);
|
||||
|
||||
assertNull(options.getRetryPolicy());
|
||||
assertEquals(retryHandler, options.getRetryHandler());
|
||||
assertNull(options.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithAppIdOnly() {
|
||||
String appId = "test-app";
|
||||
|
||||
WorkflowTaskOptions options = new WorkflowTaskOptions(appId);
|
||||
|
||||
assertNull(options.getRetryPolicy());
|
||||
assertNull(options.getRetryHandler());
|
||||
assertEquals(appId, options.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithAllParameters() {
|
||||
WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build();
|
||||
WorkflowTaskRetryHandler retryHandler = (context) -> true;
|
||||
String appId = "test-app";
|
||||
|
||||
WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy, retryHandler, appId);
|
||||
|
||||
assertEquals(retryPolicy, options.getRetryPolicy());
|
||||
assertEquals(retryHandler, options.getRetryHandler());
|
||||
assertEquals(appId, options.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithRetryPolicyAndAppId() {
|
||||
WorkflowTaskRetryPolicy retryPolicy = WorkflowTaskRetryPolicy.newBuilder().build();
|
||||
String appId = "test-app";
|
||||
|
||||
WorkflowTaskOptions options = new WorkflowTaskOptions(retryPolicy, appId);
|
||||
|
||||
assertEquals(retryPolicy, options.getRetryPolicy());
|
||||
assertNull(options.getRetryHandler());
|
||||
assertEquals(appId, options.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstructorWithRetryHandlerAndAppId() {
|
||||
WorkflowTaskRetryHandler retryHandler = (context) -> true;
|
||||
String appId = "test-app";
|
||||
|
||||
WorkflowTaskOptions options = new WorkflowTaskOptions(retryHandler, appId);
|
||||
|
||||
assertNull(options.getRetryPolicy());
|
||||
assertEquals(retryHandler, options.getRetryHandler());
|
||||
assertEquals(appId, options.getAppId());
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ package io.dapr.springboot.examples.wfp;
|
|||
|
||||
import io.dapr.testcontainers.Component;
|
||||
import io.dapr.testcontainers.DaprContainer;
|
||||
import io.dapr.testcontainers.DaprLogLevel;
|
||||
import io.github.microcks.testcontainers.MicrocksContainersEnsemble;
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
|
@ -31,6 +32,16 @@ import java.util.List;
|
|||
|
||||
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
|
||||
|
||||
/**
|
||||
* Test configuration for Dapr containers with debug logging enabled.
|
||||
*
|
||||
* This configuration sets up Dapr with DEBUG log level and console output
|
||||
* for detailed logging during test execution.
|
||||
*
|
||||
* ADDITIONAL DEBUGGING: For even more detailed logs, you can also:
|
||||
* 1. Run `docker ps` to find the Dapr container ID
|
||||
* 2. Run `docker logs --follow <container-id>` to stream real-time logs
|
||||
*/
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
public class DaprTestContainersConfig {
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,23 @@ import static org.hamcrest.Matchers.equalTo;
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* Integration tests for Dapr Workflow Patterns.
|
||||
*
|
||||
* DEBUGGING: For more detailed logs during test execution, you can:
|
||||
* 1. Run `docker ps` to find the Dapr container ID
|
||||
* 2. Run `docker logs --follow <container-id>` to stream real-time logs
|
||||
* 3. The container name will typically be something like "dapr-workflow-patterns-app-<hash>"
|
||||
*
|
||||
* Example:
|
||||
* ```bash
|
||||
* docker ps | grep dapr
|
||||
* docker logs --follow <container-id>
|
||||
* ```
|
||||
*
|
||||
* This will show you detailed Dapr runtime logs including workflow execution,
|
||||
* state transitions, and component interactions.
|
||||
*/
|
||||
@SpringBootTest(classes = {TestWorkflowPatternsApplication.class, DaprTestContainersConfig.class,
|
||||
DaprAutoConfiguration.class, },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
|
|
@ -137,6 +154,12 @@ class WorkflowPatternsAppTests {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests the ContinueAsNew workflow pattern.
|
||||
*
|
||||
* The ContinueAsNew pattern should execute cleanup activities 5 times
|
||||
* with 5-second intervals between each iteration.
|
||||
*/
|
||||
@Test
|
||||
void testContinueAsNew() {
|
||||
//This call blocks until all the clean up activities are executed
|
||||
|
|
|
|||
Loading…
Reference in New Issue