From 8f453d68090113aac706856a2aed4f0673e06aaf Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Fri, 25 Jul 2025 12:05:35 -0500 Subject: [PATCH 01/33] cross app ex Signed-off-by: Cassandra Coyle --- .../java/io/dapr/examples/workflows/README.md | 207 ++++++++++++++++++ .../crossapp/App2TransformActivity.java | 33 +++ .../workflows/crossapp/App2Worker.java | 37 ++++ .../crossapp/App3FinalizeActivity.java | 33 +++ .../workflows/crossapp/App3Worker.java | 37 ++++ .../workflows/crossapp/CrossAppWorker.java | 32 +++ .../workflows/crossapp/CrossAppWorkflow.java | 64 ++++++ .../crossapp/CrossAppWorkflowClient.java | 66 ++++++ .../dapr/workflows/WorkflowTaskOptions.java | 42 +++- .../runtime/DefaultWorkflowContext.java | 2 +- 10 files changed, 548 insertions(+), 5 deletions(-) create mode 100644 examples/src/main/java/io/dapr/examples/workflows/crossapp/App2TransformActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/crossapp/App2Worker.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/crossapp/App3FinalizeActivity.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/crossapp/App3Worker.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorker.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java create mode 100644 examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflowClient.java diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index f867dbd4c2..4ba4068519 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -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,212 @@ 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 where activities are distributed across multiple services, service mesh scenarios, or multi-tenant applications where activities are isolated by app ID. + +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 -> { + System.out.println("=== WORKFLOW STARTING ==="); + ctx.getLogger().info("Starting CrossAppWorkflow: " + ctx.getName()); + System.out.println("Workflow name: " + ctx.getName()); + System.out.println("Workflow instance ID: " + ctx.getInstanceId()); + + String input = ctx.getInput(String.class); + ctx.getLogger().info("CrossAppWorkflow received input: " + input); + System.out.println("Workflow input: " + input); + + // Call an activity in another app by passing in an active appID to the WorkflowTaskOptions + ctx.getLogger().info("Calling cross-app activity in 'app2'..."); + System.out.println("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 + ctx.getLogger().info("Calling cross-app activity in 'app3'..."); + System.out.println("About to call cross-app activity in app3..."); + String finalResult = ctx.callActivity( + App3FinalizeActivity.class.getName(), + crossAppResult, + new WorkflowTaskOptions("app3"), + String.class + ).await(); + ctx.getLogger().info("Final cross-app activity result: " + finalResult); + System.out.println("Final cross-app activity result: " + finalResult); + + ctx.getLogger().info("CrossAppWorkflow finished with: " + finalResult); + System.out.println("=== 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) { + System.out.println("=== App2: TransformActivity called ==="); + String input = ctx.getInput(String.class); + System.out.println("Input: " + input); + + // Transform the input + String result = input.toUpperCase() + " [TRANSFORMED BY APP2]"; + + System.out.println("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) { + System.out.println("=== App3: FinalizeActivity called ==="); + String input = ctx.getInput(String.class); + System.out.println("Input: " + input); + + // Finalize the processing + String result = input + " [FINALIZED BY APP3]"; + + System.out.println("Output: " + result); + return result; + } +} +``` + +**WorkflowTaskOptions Constructors for Cross-App Calls:** + +The `WorkflowTaskOptions` class supports several constructors for cross-app calls: + +```java +// App ID only +new WorkflowTaskOptions("app2") + +// Retry policy + app ID +new WorkflowTaskOptions(retryPolicy, "app2") + +// Retry handler + app ID +new WorkflowTaskOptions(retryHandler, "app2") + +// All parameters +new WorkflowTaskOptions(retryPolicy, retryHandler, "app2") +``` + +**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 --log-level=debug -- 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 --log-level=debug -- 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 --log-level=debug -- 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 +- Service mesh scenarios where different apps handle different types of activities +- 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. diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/App2TransformActivity.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App2TransformActivity.java new file mode 100644 index 0000000000..bc8b82f8b5 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App2TransformActivity.java @@ -0,0 +1,33 @@ +/* + * 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); + System.out.println("=== App2: TransformActivity called ==="); + System.out.println("Input: " + input); + String result = input.toUpperCase() + " [TRANSFORMED BY APP2]"; + System.out.println("Output: " + result); + return result; + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/App2Worker.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App2Worker.java new file mode 100644 index 0000000000..8bad2b8f5b --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App2Worker.java @@ -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(); + } + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/App3FinalizeActivity.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App3FinalizeActivity.java new file mode 100644 index 0000000000..ed9137de96 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App3FinalizeActivity.java @@ -0,0 +1,33 @@ +/* + * 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); + System.out.println("=== App3: FinalizeActivity called ==="); + System.out.println("Input: " + input); + String result = input + " [FINALIZED BY APP3]"; + System.out.println("Output: " + result); + return result; + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/App3Worker.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App3Worker.java new file mode 100644 index 0000000000..dc49baa76e --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/App3Worker.java @@ -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(); + } + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorker.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorker.java new file mode 100644 index 0000000000..ecf7dfcb6f --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorker.java @@ -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(); + } + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java new file mode 100644 index 0000000000..61e27e8e8e --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java @@ -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 -> { + System.out.println("=== WORKFLOW STARTING ==="); + ctx.getLogger().info("Starting CrossAppWorkflow: " + ctx.getName()); + System.out.println("Workflow name: " + ctx.getName()); + System.out.println("Workflow instance ID: " + ctx.getInstanceId()); + + String input = ctx.getInput(String.class); + ctx.getLogger().info("CrossAppWorkflow received input: " + input); + System.out.println("Workflow input: " + input); + + // Call an activity in another app by passing in an active appID to the WorkflowTaskOptions + ctx.getLogger().info("Calling cross-app activity in 'app2'..."); + System.out.println("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 + ctx.getLogger().info("Calling cross-app activity in 'app3'..."); + System.out.println("About to call cross-app activity in app3..."); + String finalResult = ctx.callActivity( + App3FinalizeActivity.class.getName(), + crossAppResult, + new WorkflowTaskOptions("app3"), + String.class + ).await(); + ctx.getLogger().info("Final cross-app activity result: " + finalResult); + System.out.println("Final cross-app activity result: " + finalResult); + + ctx.getLogger().info("CrossAppWorkflow finished with: " + finalResult); + System.out.println("=== WORKFLOW COMPLETING WITH: " + finalResult + " ==="); + ctx.complete(finalResult); + }; + } +} diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflowClient.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflowClient.java new file mode 100644 index 0000000000..1e7910cd2c --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflowClient.java @@ -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 "); + 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(); + } + } +} diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java index 6e16452686..fe5643d110 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowTaskOptions.java @@ -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 WorkflowTaskRetryHandler getRetryHandler() { return retryHandler; } + public String getAppId() { + return appId; + } + } diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java index f76eb00445..7da116ffc0 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java @@ -246,7 +246,7 @@ private TaskOptions toTaskOptions(WorkflowTaskOptions options) { RetryPolicy retryPolicy = toRetryPolicy(options.getRetryPolicy()); RetryHandler retryHandler = toRetryHandler(options.getRetryHandler()); - return new TaskOptions(retryPolicy, retryHandler); + return new TaskOptions(retryPolicy, retryHandler, options.getAppId()); } /** From 455467c8c1ed6c2ce05fa5aebfe4d2b7a3ac6177 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Fri, 25 Jul 2025 12:18:06 -0500 Subject: [PATCH 02/33] update protoc cmd Signed-off-by: Cassandra Coyle --- sdk-autogen/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk-autogen/pom.xml b/sdk-autogen/pom.xml index e47743a422..ab56a3dbd5 100644 --- a/sdk-autogen/pom.xml +++ b/sdk-autogen/pom.xml @@ -132,7 +132,6 @@ run - java-sdk-protoc ${protobuf.version} com.google.protobuf:protoc:3.25.5 inputs From 869cb83c35a0655c0a227e6148ebef9431eca038 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Fri, 25 Jul 2025 13:12:02 -0500 Subject: [PATCH 03/33] feedback Signed-off-by: Cassandra Coyle --- examples/src/main/java/io/dapr/examples/workflows/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 4ba4068519..064a866334 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -684,7 +684,7 @@ Key Points: ### 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 where activities are distributed across multiple services, service mesh scenarios, or multi-tenant applications where activities are isolated by app ID. +The cross-app pattern allows workflows to call activities that are hosted in different Dapr applications. This is useful for microservices architectures where activities are distributed across multiple services, or multi-tenant applications where activities are isolated by app ID. 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 @@ -885,7 +885,6 @@ The workflow demonstrates: This pattern is particularly useful for: - Microservices architectures where activities are distributed across multiple services -- Service mesh scenarios where different apps handle different types of activities - Multi-tenant applications where activities are isolated by app ID ### Suspend/Resume Pattern From 613dfb014eb710fafca8096a22e7efa9cabec5cb Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Wed, 6 Aug 2025 12:43:54 -0500 Subject: [PATCH 04/33] builder pattern Signed-off-by: Cassandra Coyle --- .../io/dapr/workflows/runtime/DefaultWorkflowContext.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java index 7da116ffc0..9fe15899a8 100644 --- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java +++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowContext.java @@ -246,7 +246,11 @@ private TaskOptions toTaskOptions(WorkflowTaskOptions options) { RetryPolicy retryPolicy = toRetryPolicy(options.getRetryPolicy()); RetryHandler retryHandler = toRetryHandler(options.getRetryHandler()); - return new TaskOptions(retryPolicy, retryHandler, options.getAppId()); + return TaskOptions.builder() + .retryPolicy(retryPolicy) + .retryHandler(retryHandler) + .appID(options.getAppId()) + .build(); } /** From c927075d8f9373a1fb2adeb014ac59ec0acb4204 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 7 Aug 2025 14:17:52 -0500 Subject: [PATCH 05/33] fix protoc Signed-off-by: Cassandra Coyle --- sdk-autogen/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk-autogen/pom.xml b/sdk-autogen/pom.xml index ab56a3dbd5..f95947a8b7 100644 --- a/sdk-autogen/pom.xml +++ b/sdk-autogen/pom.xml @@ -22,7 +22,6 @@ ${project.build.directory}/proto false 1.69.0 - java-sdk-protoc 3.25.5 From 466e7992102037db5fd1d311da73f990c37e2fc4 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 7 Aug 2025 15:21:20 -0500 Subject: [PATCH 06/33] debug log levels for test containers Signed-off-by: Cassandra Coyle --- .../examples/consumer/DaprTestContainersConfig.java | 2 +- .../springboot/examples/wfp/DaprTestContainersConfig.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java index fb3d6b5479..0b810ad6cd 100644 --- a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java +++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java @@ -93,7 +93,7 @@ public DaprContainer daprContainer(Network daprNetwork, RabbitMQContainer rabbit .withAppName("consumer-app") .withNetwork(daprNetwork).withComponent(new Component("pubsub", "pubsub.rabbitmq", "v1", rabbitMqProperties)) - .withDaprLogLevel(DaprLogLevel.INFO) + .withDaprLogLevel(DaprLogLevel.DEBUG) .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .withAppPort(8081).withAppChannelAddress("host.testcontainers.internal") .withReusablePlacement(reuse) diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java index b27900652a..1fb1f32f0a 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java @@ -15,6 +15,7 @@ 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; @@ -44,7 +45,9 @@ public DaprContainer daprContainer(Network network) { .withAppPort(8080) .withNetwork(network) .withAppHealthCheckPath("/actuator/health") - .withAppChannelAddress("host.testcontainers.internal"); + .withAppChannelAddress("host.testcontainers.internal") + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())); } From 7411384b6a6edaa2f3e84b91cc5fa78f9539d544 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Mon, 11 Aug 2025 08:18:38 -0500 Subject: [PATCH 07/33] update readme and add debugging info Signed-off-by: Cassandra Coyle --- .../java/io/dapr/examples/workflows/README.md | 19 --------------- .../wfp/DaprTestContainersConfig.java | 10 ++++++++ .../wfp/WorkflowPatternsAppTests.java | 23 +++++++++++++++++++ 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 064a866334..05481acfa7 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -838,25 +838,6 @@ expected_stdout_lines: - "Waiting for workflow completion..." - "Workflow instance with ID:" - "completed with result: HELLO WORLD [TRANSFORMED BY APP2] [FINALIZED BY APP3]" - - "=== WORKFLOW STARTING ===" - - "Starting CrossAppWorkflow:" - - "Workflow name:" - - "Workflow instance ID:" - - "CrossAppWorkflow received input: Hello World" - - "Workflow input: Hello World" - - "Calling cross-app activity in 'app2'..." - - "About to call cross-app activity in app2..." - - "=== App2: TransformActivity called ===" - - "Input: Hello World" - - "Output: HELLO WORLD [TRANSFORMED BY APP2]" - - "Calling cross-app activity in 'app3'..." - - "About to call cross-app activity in app3..." - - "=== App3: FinalizeActivity called ===" - - "Input: HELLO WORLD [TRANSFORMED BY APP2]" - - "Output: HELLO WORLD [TRANSFORMED BY APP2] [FINALIZED BY APP3]" - - "Final cross-app activity result:" - - "CrossAppWorkflow finished with:" - - "=== WORKFLOW COMPLETING WITH:" background: true sleep: 60 timeout_seconds: 60 diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java index 1fb1f32f0a..8ba41e9ef3 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java @@ -32,6 +32,16 @@ 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 ` to stream real-time logs + */ @TestConfiguration(proxyBeanMethods = false) public class DaprTestContainersConfig { diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java index 916aaa7bc6..80a9cda01a 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/WorkflowPatternsAppTests.java @@ -38,6 +38,23 @@ 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 ` to stream real-time logs + * 3. The container name will typically be something like "dapr-workflow-patterns-app-" + * + * Example: + * ```bash + * docker ps | grep dapr + * docker logs --follow + * ``` + * + * 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 @@ void testExternalEventDeny() { } + /** + * 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 From 2876fee65daa670ae9cd4390fd241051ed48ccba Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Tue, 12 Aug 2025 10:37:29 -0500 Subject: [PATCH 08/33] add IT test for cross app call activity Signed-off-by: Cassandra Coyle --- .../java/io/dapr/examples/workflows/README.md | 20 -- .../crossapp/App2TransformActivity.java | 34 ++ .../crossapp/App3FinalizeActivity.java | 34 ++ .../workflows/crossapp/App3Worker.java | 38 ++ .../workflows/crossapp/CrossAppWorker.java | 38 ++ .../workflows/crossapp/CrossAppWorkflow.java | 54 +++ .../WorkflowsCrossAppCallActivityIT.java | 327 ++++++++++++++++++ 7 files changed, 525 insertions(+), 20 deletions(-) create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 05481acfa7..a7754b2b94 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -825,26 +825,6 @@ dapr run --app-id app3 --resources-path ./components/workflows --dapr-grpc-port 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: diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java new file mode 100644 index 0000000000..534558fac7 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java @@ -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. + * 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; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java new file mode 100644 index 0000000000..c01ad3235e --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java @@ -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. + * 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; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java new file mode 100644 index 0000000000..7ac67db336 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java @@ -0,0 +1,38 @@ +/* + * 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. + * 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(); + } + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java new file mode 100644 index 0000000000..b5cc2db410 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java @@ -0,0 +1,38 @@ +/* + * 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. + * 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(); + } + } +} + diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java new file mode 100644 index 0000000000..624f6e1724 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java @@ -0,0 +1,54 @@ +/* + * 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. + * 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); + }; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java new file mode 100644 index 0000000000..058f3ed5e9 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java @@ -0,0 +1,327 @@ +/* + * 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. + * 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.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.WorkflowInstanceStatus; +import io.dapr.workflows.client.WorkflowRuntimeStatus; +import io.dapr.config.Properties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.MountableFile; + +import java.time.Duration; +import java.util.Collections; +import java.util.Map; + +import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.testcontainers.images.builder.Transferable; + +/** + * 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(); + + // Main workflow orchestrator container + @Container + private static final DaprContainer MAIN_WORKFLOW_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("crossapp-worker") + .withNetwork(DAPR_NETWORK) + .withNetworkAliases("main-workflow-sidecar") + .withComponent(new Component("kvstore", "state.in-memory", "v1", + Map.of("actorStateStore", "true"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println("MAIN_WORKFLOW: " + outputFrame.getUtf8String())) + .withAppChannelAddress("host.testcontainers.internal"); + + // App2 container for App2TransformActivity - using GenericContainer for custom ports + @Container + private static final GenericContainer APP2_CONTAINER = new GenericContainer<>(DAPR_RUNTIME_IMAGE_TAG) + .withNetwork(DAPR_NETWORK) + .withNetworkAliases("app2-sidecar") + .withExposedPorts(3501, 50002) + .withCommand("./daprd", + "--app-id", "app2", + "--dapr-listen-addresses=0.0.0.0", + "--dapr-http-port", "3501", + "--dapr-grpc-port", "50002", + "--app-protocol", "http", + "--placement-host-address", "placement:50005", + "--scheduler-host-address", "scheduler:51005", + "--app-channel-address", "main-workflow-sidecar:3500", // cant use host.testcontainers.internal because it's not a valid hostname + "--log-level", "DEBUG", + "--resources-path", "/dapr-resources") + .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + + "kind: Component\n" + + "metadata:\n" + + " name: kvstore\n" + + "spec:\n" + + " type: state.in-memory\n" + + " version: v1\n" + + " metadata:\n" + + " - name: actorStateStore\n" + + " value: 'true'\n"), "/dapr-resources/kvstore.yaml") + .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + + "kind: Component\n" + + "metadata:\n" + + " name: pubsub\n" + + "spec:\n" + + " type: pubsub.in-memory\n" + + " version: v1\n"), "/dapr-resources/pubsub.yaml") + .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + + "kind: Subscription\n" + + "metadata:\n" + + " name: local\n" + + "spec:\n" + + " pubsubname: pubsub\n" + + " topic: topic\n" + + " route: /events\n"), "/dapr-resources/subscription.yaml") + .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") + .forPort(3501) + .forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399)) + .withLogConsumer(outputFrame -> System.out.println("APP2: " + outputFrame.getUtf8String())); + + // App3 container for App3FinalizeActivity - using GenericContainer for custom ports + @Container + private static final GenericContainer APP3_CONTAINER = new GenericContainer<>(DAPR_RUNTIME_IMAGE_TAG) + .withNetwork(DAPR_NETWORK) + .withNetworkAliases("app3-sidecar") + .withExposedPorts(3502, 50003) + .withCommand("./daprd", + "--app-id", "app3", + "--dapr-listen-addresses=0.0.0.0", + "--dapr-http-port", "3502", + "--dapr-grpc-port", "50003", + "--app-protocol", "http", + "--placement-host-address", "placement:50005", + "--scheduler-host-address", "scheduler:51005", + "--app-channel-address", "main-workflow-sidecar:3500", // cant use host.testcontainers.internal because it's not a valid hostname + "--log-level", "DEBUG", + "--resources-path", "/dapr-resources") + .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + + "kind: Component\n" + + "metadata:\n" + + " name: kvstore\n" + + "spec:\n" + + " type: state.in-memory\n" + + " version: v1\n" + + " metadata:\n" + + " - name: actorStateStore\n" + + " value: 'true'\n"), "/dapr-resources/kvstore.yaml") + .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + + "kind: Component\n" + + "metadata:\n" + + " name: pubsub\n" + + "spec:\n" + + " type: pubsub.in-memory\n" + + " version: v1\n"), "/dapr-resources/pubsub.yaml") + .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + + "kind: Subscription\n" + + "metadata:\n" + + " name: local\n" + + "spec:\n" + + " pubsubname: pubsub\n" + + " topic: topic\n" + + " route: /events\n"), "/dapr-resources/subscription.yaml") + .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") + .forPort(3502) + .forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399)) + .withLogConsumer(outputFrame -> System.out.println("APP3: " + outputFrame.getUtf8String())); + + // TestContainers for each app + private static GenericContainer crossappWorker; + private static GenericContainer app2Worker; + private static GenericContainer app3Worker; + + @BeforeAll + public static void setUp() throws Exception { + // Wait for sidecars to be fully initialized & stabilize + Thread.sleep(15000); + + // Start crossapp worker (connects to MAIN_WORKFLOW_CONTAINER) + crossappWorker = new GenericContainer<>("openjdk:17-jdk-slim") + .withCopyFileToContainer(MountableFile.forHostPath("target/test-classes"), "/app/classes") + .withCopyFileToContainer(MountableFile.forHostPath("target/dependency"), "/app/libs") + .withWorkingDirectory("/app") + .withCommand("java", "-cp", "/app/classes:/app/libs/*", + "-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) + .waitingFor(Wait.forLogMessage(".*CrossAppWorker started.*", 1)) + .withLogConsumer(outputFrame -> System.out.println("CrossAppWorker: " + outputFrame.getUtf8String())); + + // Start app2 worker (connects to APP2_CONTAINER) + app2Worker = new GenericContainer<>("openjdk:17-jdk-slim") + .withCopyFileToContainer(MountableFile.forHostPath("target/test-classes"), "/app/classes") + .withCopyFileToContainer(MountableFile.forHostPath("target/dependency"), "/app/libs") + .withWorkingDirectory("/app") + .withCommand("java", "-cp", "/app/classes:/app/libs/*", + "-Ddapr.app.id=app2", + "-Ddapr.grpc.endpoint=app2-sidecar:50002", + "-Ddapr.http.endpoint=app2-sidecar:3501", + "io.dapr.it.testcontainers.workflows.crossapp.App2Worker") + .withNetwork(DAPR_NETWORK) + .waitingFor(Wait.forLogMessage(".*App2Worker started.*", 1)) + .withLogConsumer(outputFrame -> System.out.println("App2Worker: " + outputFrame.getUtf8String())); + + // Start app3 worker (connects to APP3_CONTAINER) + app3Worker = new GenericContainer<>("openjdk:17-jdk-slim") + .withCopyFileToContainer(MountableFile.forHostPath("target/test-classes"), "/app/classes") + .withCopyFileToContainer(MountableFile.forHostPath("target/dependency"), "/app/libs") + .withWorkingDirectory("/app") + .withCommand("java", "-cp", "/app/classes:/app/libs/*", + "-Ddapr.app.id=app3", + "-Ddapr.grpc.endpoint=app3-sidecar:50003", + "-Ddapr.http.endpoint=app3-sidecar:3502", + "io.dapr.it.testcontainers.workflows.crossapp.App3Worker") + .withNetwork(DAPR_NETWORK) + .waitingFor(Wait.forLogMessage(".*App3Worker started.*", 1)) + .withLogConsumer(outputFrame -> System.out.println("App3Worker: " + outputFrame.getUtf8String())); + + // Start all worker containers + crossappWorker.start(); + app2Worker.start(); + app3Worker.start(); + + // Wait for workers to be fully ready and connected + Thread.sleep(5000); + } + + @AfterAll + public static void tearDown() throws Exception { + // Clean up worker containers + if (crossappWorker != null) { + crossappWorker.stop(); + } + if (app2Worker != null) { + app2Worker.stop(); + } + if (app3Worker != null) { + app3Worker.stop(); + } + } + + /** + * Verifies that all Dapr sidecars are healthy and ready to accept connections. + * This helps prevent the "sidecar unavailable" errors we were seeing. + */ + private void verifySidecarsReady() throws Exception { + // Main container uses ports (3500, 50001) + String mainHealthUrl = "http://localhost:" + MAIN_WORKFLOW_CONTAINER.getMappedPort(3500) + "/v1.0/healthz/outbound"; + waitForHealthEndpoint(mainHealthUrl, "Main workflow sidecar"); + + // App2 uses custom ports (3501, 50002) + String app2HealthUrl = "http://localhost:" + APP2_CONTAINER.getMappedPort(3501) + "/v1.0/healthz/outbound"; + waitForHealthEndpoint(app2HealthUrl, "App2 sidecar"); + + // App3 uses custom ports (3502, 50003) + String app3HealthUrl = "http://localhost:" + APP3_CONTAINER.getMappedPort(3502) + "/v1.0/healthz/outbound"; + waitForHealthEndpoint(app3HealthUrl, "App3 sidecar"); + } + + /** + * Waits for a health endpoint to return a successful response. + */ + private void waitForHealthEndpoint(String healthUrl, String sidecarName) throws Exception { + int maxAttempts = 30; // 30s max + int attempt = 0; + + while (attempt < maxAttempts) { + try { + java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); + java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() + .uri(java.net.URI.create(healthUrl)) + .GET() + .build(); + + java.net.http.HttpResponse response = client.send(request, + java.net.http.HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() >= 200 && response.statusCode() < 400) { + System.out.println(sidecarName + " is healthy and ready"); + return; + } + } catch (Exception e) { + // Ignore connection errors bc they're expected while sidecar is starting + } + + attempt++; + Thread.sleep(1000); // Wait 1s before retry + } + + throw new RuntimeException(sidecarName + " failed to become healthy within " + maxAttempts + " seconds"); + } + + @Test + public void testCrossAppWorkflow() throws Exception { + verifySidecarsReady(); + + 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, so we need to use the mapped port + String grpcEndpoint = "localhost:" + MAIN_WORKFLOW_CONTAINER.getMappedPort(50001); + String httpEndpoint = "localhost:" + MAIN_WORKFLOW_CONTAINER.getMappedPort(3500); + System.setProperty("dapr.grpc.endpoint", grpcEndpoint); + System.setProperty("dapr.http.endpoint", httpEndpoint); + Map propertyOverrides = Map.of( + "dapr.grpc.endpoint", grpcEndpoint, + "dapr.http.endpoint", httpEndpoint + ); + + 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); + + Duration timeout = Duration.ofMinutes(2); + WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, timeout, 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(); + } + } +} From 4836553cafc13c4762cd28ab7e8257cd8447ea54 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Tue, 12 Aug 2025 17:29:29 -0500 Subject: [PATCH 09/33] cleanup test Signed-off-by: Cassandra Coyle --- .../WorkflowsCrossAppCallActivityIT.java | 276 +++++++----------- .../io/dapr/testcontainers/DaprContainer.java | 125 +++++--- 2 files changed, 188 insertions(+), 213 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java index 058f3ed5e9..f0034685dd 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java @@ -14,6 +14,8 @@ 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; @@ -22,12 +24,12 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; -import org.testcontainers.containers.wait.strategy.Wait; 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.Collections; @@ -36,7 +38,7 @@ import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import org.testcontainers.images.builder.Transferable; +import io.dapr.testcontainers.Subscription; /** * Cross-App Pattern integration test. @@ -52,113 +54,19 @@ public class WorkflowsCrossAppCallActivityIT { private static final Network DAPR_NETWORK = Network.newNetwork(); - + + // Shared placement and scheduler containers for all Dapr instances + private static DaprPlacementContainer sharedPlacementContainer; + private static DaprSchedulerContainer sharedSchedulerContainer; + // Main workflow orchestrator container - @Container - private static final DaprContainer MAIN_WORKFLOW_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) - .withAppName("crossapp-worker") - .withNetwork(DAPR_NETWORK) - .withNetworkAliases("main-workflow-sidecar") - .withComponent(new Component("kvstore", "state.in-memory", "v1", - Map.of("actorStateStore", "true"))) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) - .withDaprLogLevel(DaprLogLevel.DEBUG) - .withLogConsumer(outputFrame -> System.out.println("MAIN_WORKFLOW: " + outputFrame.getUtf8String())) - .withAppChannelAddress("host.testcontainers.internal"); - - // App2 container for App2TransformActivity - using GenericContainer for custom ports - @Container - private static final GenericContainer APP2_CONTAINER = new GenericContainer<>(DAPR_RUNTIME_IMAGE_TAG) - .withNetwork(DAPR_NETWORK) - .withNetworkAliases("app2-sidecar") - .withExposedPorts(3501, 50002) - .withCommand("./daprd", - "--app-id", "app2", - "--dapr-listen-addresses=0.0.0.0", - "--dapr-http-port", "3501", - "--dapr-grpc-port", "50002", - "--app-protocol", "http", - "--placement-host-address", "placement:50005", - "--scheduler-host-address", "scheduler:51005", - "--app-channel-address", "main-workflow-sidecar:3500", // cant use host.testcontainers.internal because it's not a valid hostname - "--log-level", "DEBUG", - "--resources-path", "/dapr-resources") - .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + - "kind: Component\n" + - "metadata:\n" + - " name: kvstore\n" + - "spec:\n" + - " type: state.in-memory\n" + - " version: v1\n" + - " metadata:\n" + - " - name: actorStateStore\n" + - " value: 'true'\n"), "/dapr-resources/kvstore.yaml") - .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + - "kind: Component\n" + - "metadata:\n" + - " name: pubsub\n" + - "spec:\n" + - " type: pubsub.in-memory\n" + - " version: v1\n"), "/dapr-resources/pubsub.yaml") - .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + - "kind: Subscription\n" + - "metadata:\n" + - " name: local\n" + - "spec:\n" + - " pubsubname: pubsub\n" + - " topic: topic\n" + - " route: /events\n"), "/dapr-resources/subscription.yaml") - .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(3501) - .forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399)) - .withLogConsumer(outputFrame -> System.out.println("APP2: " + outputFrame.getUtf8String())); - - // App3 container for App3FinalizeActivity - using GenericContainer for custom ports - @Container - private static final GenericContainer APP3_CONTAINER = new GenericContainer<>(DAPR_RUNTIME_IMAGE_TAG) - .withNetwork(DAPR_NETWORK) - .withNetworkAliases("app3-sidecar") - .withExposedPorts(3502, 50003) - .withCommand("./daprd", - "--app-id", "app3", - "--dapr-listen-addresses=0.0.0.0", - "--dapr-http-port", "3502", - "--dapr-grpc-port", "50003", - "--app-protocol", "http", - "--placement-host-address", "placement:50005", - "--scheduler-host-address", "scheduler:51005", - "--app-channel-address", "main-workflow-sidecar:3500", // cant use host.testcontainers.internal because it's not a valid hostname - "--log-level", "DEBUG", - "--resources-path", "/dapr-resources") - .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + - "kind: Component\n" + - "metadata:\n" + - " name: kvstore\n" + - "spec:\n" + - " type: state.in-memory\n" + - " version: v1\n" + - " metadata:\n" + - " - name: actorStateStore\n" + - " value: 'true'\n"), "/dapr-resources/kvstore.yaml") - .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + - "kind: Component\n" + - "metadata:\n" + - " name: pubsub\n" + - "spec:\n" + - " type: pubsub.in-memory\n" + - " version: v1\n"), "/dapr-resources/pubsub.yaml") - .withCopyToContainer(Transferable.of("apiVersion: dapr.io/v1alpha1\n" + - "kind: Subscription\n" + - "metadata:\n" + - " name: local\n" + - "spec:\n" + - " pubsubname: pubsub\n" + - " topic: topic\n" + - " route: /events\n"), "/dapr-resources/subscription.yaml") - .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(3502) - .forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399)) - .withLogConsumer(outputFrame -> System.out.println("APP3: " + outputFrame.getUtf8String())); + private static DaprContainer MAIN_WORKFLOW_CONTAINER; + + // App2 container for App2TransformActivity - using DaprContainer with custom ports + private static DaprContainer APP2_CONTAINER; + + // App3 container for App3FinalizeActivity - using DaprContainer with custom ports + private static DaprContainer APP3_CONTAINER; // TestContainers for each app private static GenericContainer crossappWorker; @@ -167,9 +75,81 @@ public class WorkflowsCrossAppCallActivityIT { @BeforeAll public static void setUp() throws Exception { - // Wait for sidecars to be fully initialized & stabilize - Thread.sleep(15000); + sharedPlacementContainer = new DaprPlacementContainer("daprio/placement:1.16.0-rc.3") + .withNetwork(DAPR_NETWORK) + .withNetworkAliases("placement") + .withReuse(false); + sharedPlacementContainer.start(); + + sharedSchedulerContainer = new DaprSchedulerContainer("daprio/scheduler:1.16.0-rc.3") + .withNetwork(DAPR_NETWORK) + .withNetworkAliases("scheduler") + .withReuse(false); + sharedSchedulerContainer.start(); + + // Initialize Dapr containers with shared placement and scheduler + MAIN_WORKFLOW_CONTAINER = 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"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) + .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println("MAIN_WORKFLOW: " + outputFrame.getUtf8String())) + .withAppChannelAddress("host.testcontainers.internal") + .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") + .forPort(3500) + .forStatusCode(200) + .forStatusCode(204)); + + APP2_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("app2") + .withNetwork(DAPR_NETWORK) + .withNetworkAliases("app2-sidecar") + .withPlacementContainer(sharedPlacementContainer) + .withSchedulerContainer(sharedSchedulerContainer) + .withCustomHttpPort(3501) + .withCustomGrpcPort(50002) + .withAppChannelAddress("main-workflow-sidecar:3500") + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withComponent(new Component("kvstore", "state.in-memory", "v1", + Map.of("actorStateStore", "true"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) + .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) + .withLogConsumer(outputFrame -> System.out.println("APP2: " + outputFrame.getUtf8String())) + .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") + .forPort(3501) + .forStatusCode(200) + .forStatusCode(204)); + APP3_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) + .withAppName("app3") + .withNetwork(DAPR_NETWORK) + .withNetworkAliases("app3-sidecar") + .withPlacementContainer(sharedPlacementContainer) + .withSchedulerContainer(sharedSchedulerContainer) + .withCustomHttpPort(3502) + .withCustomGrpcPort(50003) + .withAppChannelAddress("main-workflow-sidecar:3500") + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withComponent(new Component("kvstore", "state.in-memory", "v1", + Map.of("actorStateStore", "true"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) + .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) + .withLogConsumer(outputFrame -> System.out.println("APP3: " + outputFrame.getUtf8String())) + .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") + .forPort(3502) + .forStatusCode(200) + .forStatusCode(204)); + + // Start the Dapr containers + MAIN_WORKFLOW_CONTAINER.start(); + APP2_CONTAINER.start(); + APP3_CONTAINER.start(); + // Start crossapp worker (connects to MAIN_WORKFLOW_CONTAINER) crossappWorker = new GenericContainer<>("openjdk:17-jdk-slim") .withCopyFileToContainer(MountableFile.forHostPath("target/test-classes"), "/app/classes") @@ -216,14 +196,10 @@ public static void setUp() throws Exception { crossappWorker.start(); app2Worker.start(); app3Worker.start(); - - // Wait for workers to be fully ready and connected - Thread.sleep(5000); } @AfterAll - public static void tearDown() throws Exception { - // Clean up worker containers + public static void tearDown() { if (crossappWorker != null) { crossappWorker.stop(); } @@ -233,63 +209,27 @@ public static void tearDown() throws Exception { if (app3Worker != null) { app3Worker.stop(); } - } - - /** - * Verifies that all Dapr sidecars are healthy and ready to accept connections. - * This helps prevent the "sidecar unavailable" errors we were seeing. - */ - private void verifySidecarsReady() throws Exception { - // Main container uses ports (3500, 50001) - String mainHealthUrl = "http://localhost:" + MAIN_WORKFLOW_CONTAINER.getMappedPort(3500) + "/v1.0/healthz/outbound"; - waitForHealthEndpoint(mainHealthUrl, "Main workflow sidecar"); - - // App2 uses custom ports (3501, 50002) - String app2HealthUrl = "http://localhost:" + APP2_CONTAINER.getMappedPort(3501) + "/v1.0/healthz/outbound"; - waitForHealthEndpoint(app2HealthUrl, "App2 sidecar"); - - // App3 uses custom ports (3502, 50003) - String app3HealthUrl = "http://localhost:" + APP3_CONTAINER.getMappedPort(3502) + "/v1.0/healthz/outbound"; - waitForHealthEndpoint(app3HealthUrl, "App3 sidecar"); - } - - /** - * Waits for a health endpoint to return a successful response. - */ - private void waitForHealthEndpoint(String healthUrl, String sidecarName) throws Exception { - int maxAttempts = 30; // 30s max - int attempt = 0; - - while (attempt < maxAttempts) { - try { - java.net.http.HttpClient client = java.net.http.HttpClient.newHttpClient(); - java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder() - .uri(java.net.URI.create(healthUrl)) - .GET() - .build(); - - java.net.http.HttpResponse response = client.send(request, - java.net.http.HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() >= 200 && response.statusCode() < 400) { - System.out.println(sidecarName + " is healthy and ready"); - return; - } - } catch (Exception e) { - // Ignore connection errors bc they're expected while sidecar is starting - } - - attempt++; - Thread.sleep(1000); // Wait 1s before retry + if (MAIN_WORKFLOW_CONTAINER != null) { + MAIN_WORKFLOW_CONTAINER.stop(); + } + if (APP2_CONTAINER != null) { + APP2_CONTAINER.stop(); + } + if (APP3_CONTAINER != null) { + APP3_CONTAINER.stop(); + } + if (sharedPlacementContainer != null) { + sharedPlacementContainer.stop(); + } + if (sharedSchedulerContainer != null) { + sharedSchedulerContainer.stop(); } - - throw new RuntimeException(sidecarName + " failed to become healthy within " + maxAttempts + " seconds"); } @Test public void testCrossAppWorkflow() throws Exception { - verifySidecarsReady(); - + // 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]"; diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 51dce6f076..0845a6994f 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -39,6 +39,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.UUID; import static io.dapr.testcontainers.DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG; import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; @@ -83,6 +84,9 @@ public class DaprContainer extends GenericContainer { private boolean shouldReusePlacement; private boolean shouldReuseScheduler; + private Integer customHttpPort; + private Integer customGrpcPort; + /** * Creates a new Dapr container. * @@ -166,6 +170,16 @@ public DaprContainer withSchedulerService(String schedulerService) { return this; } + public DaprContainer withCustomHttpPort(Integer port) { + this.customHttpPort = port; + return this; + } + + public DaprContainer withCustomGrpcPort(Integer port) { + this.customGrpcPort = port; + return this; + } + public DaprContainer withAppName(String appName) { this.appName = appName; return this; @@ -287,6 +301,42 @@ protected void configure() { withNetwork(Network.newNetwork()); } + if (configuration != null) { + String configurationYaml = CONFIGURATION_CONVERTER.convert(configuration); + + LOGGER.info("> Configuration YAML: \n"); + LOGGER.info("\t\n" + configurationYaml + "\n"); + + withCopyToContainer(Transferable.of(configurationYaml), "/dapr-resources/" + configuration.getName() + ".yaml"); + } + + for (Component component : components) { + String componentYaml = COMPONENT_CONVERTER.convert(component); + + LOGGER.info("> Component YAML: \n"); + LOGGER.info("\t\n" + componentYaml + "\n"); + + withCopyToContainer(Transferable.of(componentYaml), "/dapr-resources/" + component.getName() + ".yaml"); + } + + for (Subscription subscription : subscriptions) { + String subscriptionYaml = SUBSCRIPTION_CONVERTER.convert(subscription); + + LOGGER.info("> Subscription YAML: \n"); + LOGGER.info("\t\n" + subscriptionYaml + "\n"); + + withCopyToContainer(Transferable.of(subscriptionYaml), "/dapr-resources/" + subscription.getName() + ".yaml"); + } + + for (HttpEndpoint endpoint : httpEndpoints) { + String endpointYaml = HTTPENDPOINT_CONVERTER.convert(endpoint); + + LOGGER.info("> HTTPEndpoint YAML: \n"); + LOGGER.info("\t\n" + endpointYaml + "\n"); + + withCopyToContainer(Transferable.of(endpointYaml), "/dapr-resources/" + endpoint.getName() + ".yaml"); + } + if (this.placementContainer == null) { this.placementContainer = new DaprPlacementContainer(this.placementDockerImageName) .withNetwork(getNetwork()) @@ -303,11 +353,33 @@ protected void configure() { this.schedulerContainer.start(); } + dependsOn(placementContainer, schedulerContainer); + } + + @Override + public void start() { List cmds = new ArrayList<>(); cmds.add("./daprd"); + + String finalAppName = (appName != null && !appName.trim().isEmpty()) + ? appName + : "dapr-app-" + UUID.randomUUID().toString().substring(0, 8); + cmds.add("--app-id"); - cmds.add(appName); + cmds.add(finalAppName); + cmds.add("--dapr-listen-addresses=0.0.0.0"); + + if (customHttpPort != null) { + cmds.add("--dapr-http-port"); + cmds.add(Integer.toString(customHttpPort)); + } + + if (customGrpcPort != null) { + cmds.add("--dapr-grpc-port"); + cmds.add(Integer.toString(customGrpcPort)); + } + cmds.add("--app-protocol"); cmds.add(DAPR_PROTOCOL.getName()); cmds.add("--placement-host-address"); @@ -357,52 +429,15 @@ protected void configure() { withCommand(cmdArray); - if (configuration != null) { - String configurationYaml = CONFIGURATION_CONVERTER.convert(configuration); - - LOGGER.info("> Configuration YAML: \n"); - LOGGER.info("\t\n" + configurationYaml + "\n"); - - withCopyToContainer(Transferable.of(configurationYaml), "/dapr-resources/" + configuration.getName() + ".yaml"); - } - - if (components.isEmpty()) { - components.add(new Component("kvstore", "state.in-memory", "v1", Collections.emptyMap())); - components.add(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())); - } - - if (subscriptions.isEmpty() && !components.isEmpty()) { - subscriptions.add(new Subscription("local", "pubsub", "topic", "/events")); - } - - for (Component component : components) { - String componentYaml = COMPONENT_CONVERTER.convert(component); - - LOGGER.info("> Component YAML: \n"); - LOGGER.info("\t\n" + componentYaml + "\n"); - - withCopyToContainer(Transferable.of(componentYaml), "/dapr-resources/" + component.getName() + ".yaml"); - } - - for (Subscription subscription : subscriptions) { - String subscriptionYaml = SUBSCRIPTION_CONVERTER.convert(subscription); - - LOGGER.info("> Subscription YAML: \n"); - LOGGER.info("\t\n" + subscriptionYaml + "\n"); - - withCopyToContainer(Transferable.of(subscriptionYaml), "/dapr-resources/" + subscription.getName() + ".yaml"); + // Update exposed ports and wait strategy if custom ports are specified + if (customHttpPort != null && customGrpcPort != null) { + withExposedPorts(customHttpPort, customGrpcPort); + setWaitStrategy(Wait.forHttp("/v1.0/healthz/outbound") + .forPort(customHttpPort) + .forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399)); } - for (HttpEndpoint endpoint : httpEndpoints) { - String endpointYaml = HTTPENDPOINT_CONVERTER.convert(endpoint); - - LOGGER.info("> HTTPEndpoint YAML: \n"); - LOGGER.info("\t\n" + endpointYaml + "\n"); - - withCopyToContainer(Transferable.of(endpointYaml), "/dapr-resources/" + endpoint.getName() + ".yaml"); - } - - dependsOn(placementContainer, schedulerContainer); + super.start(); } public String getAppName() { From 8a8b36e9a921c79d5c6ebd1edd8aab638145bd00 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Wed, 13 Aug 2025 10:49:02 -0500 Subject: [PATCH 10/33] sysout -> ctx.logger Signed-off-by: Cassandra Coyle --- .../java/io/dapr/examples/workflows/README.md | 75 ++++++++++--------- .../workflows/crossapp/CrossAppWorkflow.java | 16 ++-- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index a7754b2b94..7052391c64 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -684,51 +684,52 @@ Key Points: ### 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 where activities are distributed across multiple services, or multi-tenant applications where activities are isolated by app ID. +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 -> { - System.out.println("=== WORKFLOW STARTING ==="); - ctx.getLogger().info("Starting CrossAppWorkflow: " + ctx.getName()); - System.out.println("Workflow name: " + ctx.getName()); - System.out.println("Workflow instance ID: " + ctx.getInstanceId()); - - String input = ctx.getInput(String.class); - ctx.getLogger().info("CrossAppWorkflow received input: " + input); - System.out.println("Workflow input: " + input); - - // Call an activity in another app by passing in an active appID to the WorkflowTaskOptions - ctx.getLogger().info("Calling cross-app activity in 'app2'..."); - System.out.println("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 - ctx.getLogger().info("Calling cross-app activity in 'app3'..."); - System.out.println("About to call cross-app activity in app3..."); - String finalResult = ctx.callActivity( - App3FinalizeActivity.class.getName(), - crossAppResult, - new WorkflowTaskOptions("app3"), - String.class - ).await(); - ctx.getLogger().info("Final cross-app activity result: " + finalResult); - System.out.println("Final cross-app activity result: " + finalResult); - - ctx.getLogger().info("CrossAppWorkflow finished with: " + finalResult); - System.out.println("=== WORKFLOW COMPLETING WITH: " + finalResult + " ==="); - ctx.complete(finalResult); - }; + return ctx -> { + ctx.getLogger().info("=== WORKFLOW STARTING ==="); + ctx.getLogger().info("Starting CrossAppWorkflow: " + ctx.getName()); + ctx.getLogger().info("Workflow name: " + ctx.getName()); + ctx.getLogger().info("Workflow instance ID: " + ctx.getInstanceId()); + + String input = ctx.getInput(String.class); + ctx.getLogger().info("CrossAppWorkflow received input: " + input); + ctx.getLogger().info("Workflow input: " + input); + + // Call an activity in another app by passing in an active appID to the WorkflowTaskOptions + ctx.getLogger().info("Calling cross-app activity in 'app2'..."); + ctx.getLogger().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 + ctx.getLogger().info("Calling cross-app activity in 'app3'..."); + ctx.getLogger().info("About to call cross-app activity in app3..."); + String finalResult = ctx.callActivity( + App3FinalizeActivity.class.getName(), + crossAppResult, + new WorkflowTaskOptions("app3"), + String.class + ).await(); + ctx.getLogger().info("Final cross-app activity result: " + finalResult); + ctx.getLogger().info("Final cross-app activity result: " + finalResult); + + ctx.getLogger().info("CrossAppWorkflow finished with: " + finalResult); + ctx.getLogger().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: diff --git a/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java index 61e27e8e8e..32a02d7a84 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java +++ b/examples/src/main/java/io/dapr/examples/workflows/crossapp/CrossAppWorkflow.java @@ -25,18 +25,18 @@ public class CrossAppWorkflow implements Workflow { @Override public WorkflowStub create() { return ctx -> { - System.out.println("=== WORKFLOW STARTING ==="); + ctx.getLogger().info("=== WORKFLOW STARTING ==="); ctx.getLogger().info("Starting CrossAppWorkflow: " + ctx.getName()); - System.out.println("Workflow name: " + ctx.getName()); - System.out.println("Workflow instance ID: " + ctx.getInstanceId()); + ctx.getLogger().info("Workflow name: " + ctx.getName()); + ctx.getLogger().info("Workflow instance ID: " + ctx.getInstanceId()); String input = ctx.getInput(String.class); ctx.getLogger().info("CrossAppWorkflow received input: " + input); - System.out.println("Workflow input: " + input); + ctx.getLogger().info("Workflow input: " + input); // Call an activity in another app by passing in an active appID to the WorkflowTaskOptions ctx.getLogger().info("Calling cross-app activity in 'app2'..."); - System.out.println("About to call cross-app activity in app2..."); + ctx.getLogger().info("About to call cross-app activity in app2..."); String crossAppResult = ctx.callActivity( App2TransformActivity.class.getName(), input, @@ -46,7 +46,7 @@ public WorkflowStub create() { // Call another activity in a different app ctx.getLogger().info("Calling cross-app activity in 'app3'..."); - System.out.println("About to call cross-app activity in app3..."); + ctx.getLogger().info("About to call cross-app activity in app3..."); String finalResult = ctx.callActivity( App3FinalizeActivity.class.getName(), crossAppResult, @@ -54,10 +54,10 @@ public WorkflowStub create() { String.class ).await(); ctx.getLogger().info("Final cross-app activity result: " + finalResult); - System.out.println("Final cross-app activity result: " + finalResult); + ctx.getLogger().info("Final cross-app activity result: " + finalResult); ctx.getLogger().info("CrossAppWorkflow finished with: " + finalResult); - System.out.println("=== WORKFLOW COMPLETING WITH: " + finalResult + " ==="); + ctx.getLogger().info("=== WORKFLOW COMPLETING WITH: " + finalResult + " ==="); ctx.complete(finalResult); }; } From af2e57dbb9844c73afea08fa4690b1ce46111fca Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Wed, 20 Aug 2025 13:29:00 -0500 Subject: [PATCH 11/33] reset pom Signed-off-by: Cassandra Coyle --- sdk-autogen/pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sdk-autogen/pom.xml b/sdk-autogen/pom.xml index f95947a8b7..1f8683994d 100644 --- a/sdk-autogen/pom.xml +++ b/sdk-autogen/pom.xml @@ -22,6 +22,7 @@ ${project.build.directory}/proto false 1.69.0 + protoc 3.25.5 @@ -131,6 +132,7 @@ run + protoc ${protobuf.version} com.google.protobuf:protoc:3.25.5 inputs From fcaf69976f35bece9415bc903491426e8df241f9 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Wed, 20 Aug 2025 13:30:06 -0500 Subject: [PATCH 12/33] rm debug lines from readme Signed-off-by: Cassandra Coyle --- examples/src/main/java/io/dapr/examples/workflows/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 7052391c64..6aab2cbb8a 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -808,17 +808,17 @@ This example requires running multiple Dapr applications simultaneously. You'll 1. **Start the main workflow worker (crossapp-worker):** ```sh -dapr run --app-id crossapp-worker --resources-path ./components/workflows --dapr-grpc-port 50001 --log-level=debug -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.crossapp.CrossAppWorker +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 --log-level=debug -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.crossapp.App2Worker +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 --log-level=debug -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.crossapp.App3Worker +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:** From b5f541bc34302d9fbd8750476838959794a44abd Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 13:12:38 -0500 Subject: [PATCH 13/33] fix header + rm customports Signed-off-by: Cassandra Coyle --- .../crossapp/App2TransformActivity.java | 4 +- .../workflows/crossapp/App2Worker.java | 40 +++++++++++ .../crossapp/App3FinalizeActivity.java | 4 +- .../workflows/crossapp/App3Worker.java | 4 +- .../workflows/crossapp/CrossAppWorker.java | 4 +- .../workflows/crossapp/CrossAppWorkflow.java | 6 +- .../WorkflowsCrossAppCallActivityIT.java | 66 ++++++++++++++----- .../io/dapr/testcontainers/DaprContainer.java | 29 +------- 8 files changed, 107 insertions(+), 50 deletions(-) create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2Worker.java diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java index 534558fac7..0fa4733062 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2TransformActivity.java @@ -2,12 +2,14 @@ * 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; diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2Worker.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2Worker.java new file mode 100644 index 0000000000..f9748f302f --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App2Worker.java @@ -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(); + } + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java index c01ad3235e..e74976b4c8 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3FinalizeActivity.java @@ -2,12 +2,14 @@ * 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; diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java index 7ac67db336..be1d8de468 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/App3Worker.java @@ -2,12 +2,14 @@ * 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; diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java index b5cc2db410..36a2fdf791 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorker.java @@ -2,12 +2,14 @@ * 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; diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java index 624f6e1724..514f7c59e3 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/CrossAppWorkflow.java @@ -2,12 +2,14 @@ * 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; @@ -18,6 +20,8 @@ public class CrossAppWorkflow implements Workflow { + + @Override public WorkflowStub create() { return ctx -> { diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java index f0034685dd..3dff1bba4e 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java @@ -2,12 +2,14 @@ * 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; @@ -34,6 +36,7 @@ import java.time.Duration; import java.util.Collections; import java.util.Map; +import java.io.File; import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -75,6 +78,9 @@ public class WorkflowsCrossAppCallActivityIT { @BeforeAll public static void setUp() throws Exception { + // Ensure dependencies are copied for container classpath + ensureDependenciesCopied(); + sharedPlacementContainer = new DaprPlacementContainer("daprio/placement:1.16.0-rc.3") .withNetwork(DAPR_NETWORK) .withNetworkAliases("placement") @@ -111,8 +117,6 @@ public static void setUp() throws Exception { .withNetworkAliases("app2-sidecar") .withPlacementContainer(sharedPlacementContainer) .withSchedulerContainer(sharedSchedulerContainer) - .withCustomHttpPort(3501) - .withCustomGrpcPort(50002) .withAppChannelAddress("main-workflow-sidecar:3500") .withDaprLogLevel(DaprLogLevel.DEBUG) .withComponent(new Component("kvstore", "state.in-memory", "v1", @@ -120,8 +124,9 @@ public static void setUp() throws Exception { .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) .withLogConsumer(outputFrame -> System.out.println("APP2: " + outputFrame.getUtf8String())) + .withExposedPorts(3500, 50001) .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(3501) + .forPort(3500) .forStatusCode(200) .forStatusCode(204)); @@ -131,8 +136,6 @@ public static void setUp() throws Exception { .withNetworkAliases("app3-sidecar") .withPlacementContainer(sharedPlacementContainer) .withSchedulerContainer(sharedSchedulerContainer) - .withCustomHttpPort(3502) - .withCustomGrpcPort(50003) .withAppChannelAddress("main-workflow-sidecar:3500") .withDaprLogLevel(DaprLogLevel.DEBUG) .withComponent(new Component("kvstore", "state.in-memory", "v1", @@ -140,16 +143,12 @@ public static void setUp() throws Exception { .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) .withLogConsumer(outputFrame -> System.out.println("APP3: " + outputFrame.getUtf8String())) + .withExposedPorts(3500, 50001) .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(3502) + .forPort(3500) .forStatusCode(200) .forStatusCode(204)); - // Start the Dapr containers - MAIN_WORKFLOW_CONTAINER.start(); - APP2_CONTAINER.start(); - APP3_CONTAINER.start(); - // Start crossapp worker (connects to MAIN_WORKFLOW_CONTAINER) crossappWorker = new GenericContainer<>("openjdk:17-jdk-slim") .withCopyFileToContainer(MountableFile.forHostPath("target/test-classes"), "/app/classes") @@ -164,20 +163,25 @@ public static void setUp() throws Exception { .waitingFor(Wait.forLogMessage(".*CrossAppWorker started.*", 1)) .withLogConsumer(outputFrame -> System.out.println("CrossAppWorker: " + outputFrame.getUtf8String())); - // Start app2 worker (connects to APP2_CONTAINER) + // Start all Dapr containers + MAIN_WORKFLOW_CONTAINER.start(); + APP2_CONTAINER.start(); + APP3_CONTAINER.start(); + + // Start app2 worker (connects to APP2_CONTAINER) app2Worker = new GenericContainer<>("openjdk:17-jdk-slim") .withCopyFileToContainer(MountableFile.forHostPath("target/test-classes"), "/app/classes") .withCopyFileToContainer(MountableFile.forHostPath("target/dependency"), "/app/libs") .withWorkingDirectory("/app") .withCommand("java", "-cp", "/app/classes:/app/libs/*", "-Ddapr.app.id=app2", - "-Ddapr.grpc.endpoint=app2-sidecar:50002", - "-Ddapr.http.endpoint=app2-sidecar:3501", + "-Ddapr.grpc.endpoint=app2-sidecar:50001", + "-Ddapr.http.endpoint=app2-sidecar:3500", "io.dapr.it.testcontainers.workflows.crossapp.App2Worker") .withNetwork(DAPR_NETWORK) .waitingFor(Wait.forLogMessage(".*App2Worker started.*", 1)) .withLogConsumer(outputFrame -> System.out.println("App2Worker: " + outputFrame.getUtf8String())); - + // Start app3 worker (connects to APP3_CONTAINER) app3Worker = new GenericContainer<>("openjdk:17-jdk-slim") .withCopyFileToContainer(MountableFile.forHostPath("target/test-classes"), "/app/classes") @@ -185,8 +189,8 @@ public static void setUp() throws Exception { .withWorkingDirectory("/app") .withCommand("java", "-cp", "/app/classes:/app/libs/*", "-Ddapr.app.id=app3", - "-Ddapr.grpc.endpoint=app3-sidecar:50003", - "-Ddapr.http.endpoint=app3-sidecar:3502", + "-Ddapr.grpc.endpoint=app3-sidecar:50001", + "-Ddapr.http.endpoint=app3-sidecar:3500", "io.dapr.it.testcontainers.workflows.crossapp.App3Worker") .withNetwork(DAPR_NETWORK) .waitingFor(Wait.forLogMessage(".*App3Worker started.*", 1)) @@ -264,4 +268,30 @@ public void testCrossAppWorkflow() throws Exception { workflowClient.close(); } } + + /** + * Ensures that dependencies are copied to target/dependency for container classpath. + * This is needed because the containers need access to the workflow runtime classes. + */ + private static void ensureDependenciesCopied() { + File dependencyDir = new File("target/dependency"); + if (!dependencyDir.exists() || dependencyDir.listFiles() == null || dependencyDir.listFiles().length == 0) { + System.out.println("Dependencies not found in target/dependency, copying..."); + try { + ProcessBuilder pb = new ProcessBuilder("mvn", "dependency:copy-dependencies", + "-Dspotbugs.skip=true", "-Dcheckstyle.skip=true"); + pb.inheritIO(); + Process process = pb.start(); + int exitCode = process.waitFor(); + if (exitCode != 0) { + throw new RuntimeException("Failed to copy dependencies, exit code: " + exitCode); + } + System.out.println("Dependencies copied successfully"); + } catch (Exception e) { + throw new RuntimeException("Failed to ensure dependencies are copied", e); + } + } else { + System.out.println("Dependencies already exist in target/dependency"); + } + } } diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 0845a6994f..3517e10490 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -84,8 +84,7 @@ public class DaprContainer extends GenericContainer { private boolean shouldReusePlacement; private boolean shouldReuseScheduler; - private Integer customHttpPort; - private Integer customGrpcPort; + /** * Creates a new Dapr container. @@ -170,15 +169,7 @@ public DaprContainer withSchedulerService(String schedulerService) { return this; } - public DaprContainer withCustomHttpPort(Integer port) { - this.customHttpPort = port; - return this; - } - public DaprContainer withCustomGrpcPort(Integer port) { - this.customGrpcPort = port; - return this; - } public DaprContainer withAppName(String appName) { this.appName = appName; @@ -370,16 +361,6 @@ public void start() { cmds.add("--dapr-listen-addresses=0.0.0.0"); - if (customHttpPort != null) { - cmds.add("--dapr-http-port"); - cmds.add(Integer.toString(customHttpPort)); - } - - if (customGrpcPort != null) { - cmds.add("--dapr-grpc-port"); - cmds.add(Integer.toString(customGrpcPort)); - } - cmds.add("--app-protocol"); cmds.add(DAPR_PROTOCOL.getName()); cmds.add("--placement-host-address"); @@ -429,13 +410,7 @@ public void start() { withCommand(cmdArray); - // Update exposed ports and wait strategy if custom ports are specified - if (customHttpPort != null && customGrpcPort != null) { - withExposedPorts(customHttpPort, customGrpcPort); - setWaitStrategy(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(customHttpPort) - .forStatusCodeMatching(statusCode -> statusCode >= 200 && statusCode <= 399)); - } + super.start(); } From 710ba9db4127b6d2baf5911045e6912ed05e4b96 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 13:15:51 -0500 Subject: [PATCH 14/33] use consts Signed-off-by: Cassandra Coyle --- .../workflows/crossapp/WorkflowsCrossAppCallActivityIT.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java index 3dff1bba4e..fa684ecf59 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java @@ -39,6 +39,8 @@ import java.io.File; 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; import io.dapr.testcontainers.Subscription; @@ -81,13 +83,13 @@ public static void setUp() throws Exception { // Ensure dependencies are copied for container classpath ensureDependenciesCopied(); - sharedPlacementContainer = new DaprPlacementContainer("daprio/placement:1.16.0-rc.3") + sharedPlacementContainer = new DaprPlacementContainer(DAPR_PLACEMENT_IMAGE_TAG) .withNetwork(DAPR_NETWORK) .withNetworkAliases("placement") .withReuse(false); sharedPlacementContainer.start(); - sharedSchedulerContainer = new DaprSchedulerContainer("daprio/scheduler:1.16.0-rc.3") + sharedSchedulerContainer = new DaprSchedulerContainer(DAPR_SCHEDULER_IMAGE_TAG) .withNetwork(DAPR_NETWORK) .withNetworkAliases("scheduler") .withReuse(false); From 0d5c425eda2bfa642d6b6a669ebf26375251aa10 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 13:30:10 -0500 Subject: [PATCH 15/33] rm waitfor call Signed-off-by: Cassandra Coyle --- .../crossapp/WorkflowsCrossAppCallActivityIT.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java index fa684ecf59..ab2c339157 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java @@ -126,11 +126,7 @@ public static void setUp() throws Exception { .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) .withLogConsumer(outputFrame -> System.out.println("APP2: " + outputFrame.getUtf8String())) - .withExposedPorts(3500, 50001) - .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(3500) - .forStatusCode(200) - .forStatusCode(204)); + .withExposedPorts(3500, 50001); APP3_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) .withAppName("app3") @@ -145,11 +141,7 @@ public static void setUp() throws Exception { .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) .withLogConsumer(outputFrame -> System.out.println("APP3: " + outputFrame.getUtf8String())) - .withExposedPorts(3500, 50001) - .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(3500) - .forStatusCode(200) - .forStatusCode(204)); + .withExposedPorts(3500, 50001); // Start crossapp worker (connects to MAIN_WORKFLOW_CONTAINER) crossappWorker = new GenericContainer<>("openjdk:17-jdk-slim") From b0e5e2324076bd88aee52618381d2380996753c6 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 13:34:19 -0500 Subject: [PATCH 16/33] rm pubsub Signed-off-by: Cassandra Coyle --- .../WorkflowsCrossAppCallActivityIT.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java index ab2c339157..772ef999d9 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java @@ -103,15 +103,9 @@ public static void setUp() throws Exception { .withPlacementContainer(sharedPlacementContainer) .withSchedulerContainer(sharedSchedulerContainer) .withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true"))) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) - .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) .withDaprLogLevel(DaprLogLevel.DEBUG) .withLogConsumer(outputFrame -> System.out.println("MAIN_WORKFLOW: " + outputFrame.getUtf8String())) - .withAppChannelAddress("host.testcontainers.internal") - .waitingFor(Wait.forHttp("/v1.0/healthz/outbound") - .forPort(3500) - .forStatusCode(200) - .forStatusCode(204)); + .withAppChannelAddress("host.testcontainers.internal"); APP2_CONTAINER = new DaprContainer(DAPR_RUNTIME_IMAGE_TAG) .withAppName("app2") @@ -121,10 +115,7 @@ public static void setUp() throws Exception { .withSchedulerContainer(sharedSchedulerContainer) .withAppChannelAddress("main-workflow-sidecar:3500") .withDaprLogLevel(DaprLogLevel.DEBUG) - .withComponent(new Component("kvstore", "state.in-memory", "v1", - Map.of("actorStateStore", "true"))) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) - .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) + .withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true"))) .withLogConsumer(outputFrame -> System.out.println("APP2: " + outputFrame.getUtf8String())) .withExposedPorts(3500, 50001); @@ -136,10 +127,7 @@ public static void setUp() throws Exception { .withSchedulerContainer(sharedSchedulerContainer) .withAppChannelAddress("main-workflow-sidecar:3500") .withDaprLogLevel(DaprLogLevel.DEBUG) - .withComponent(new Component("kvstore", "state.in-memory", "v1", - Map.of("actorStateStore", "true"))) - .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) - .withSubscription(new Subscription("local", "pubsub", "topic", "/events")) + .withComponent(new Component("kvstore", "state.in-memory", "v1", Map.of("actorStateStore", "true"))) .withLogConsumer(outputFrame -> System.out.println("APP3: " + outputFrame.getUtf8String())) .withExposedPorts(3500, 50001); From 015d2001632f3b5c33543b2b41489ce80b2a1c5b Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 13:44:58 -0500 Subject: [PATCH 17/33] rm timeout Signed-off-by: Cassandra Coyle --- .../workflows/crossapp/WorkflowsCrossAppCallActivityIT.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java index 772ef999d9..3d63a6ac20 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/crossapp/WorkflowsCrossAppCallActivityIT.java @@ -239,8 +239,7 @@ public void testCrossAppWorkflow() throws Exception { assertNotNull(instanceId, "Workflow instance ID should not be null"); workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(30), false); - Duration timeout = Duration.ofMinutes(2); - WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, timeout, true); + WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, null, true); assertNotNull(workflowStatus, "Workflow status should not be null"); assertEquals(WorkflowRuntimeStatus.COMPLETED, workflowStatus.getRuntimeStatus(), "Workflow should complete successfully"); From 46f6c3b265281723a8bc9fc97223a136ec11fc54 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 16:32:12 -0500 Subject: [PATCH 18/33] reset empty lines added Signed-off-by: Cassandra Coyle --- .../springboot/examples/wfp/DaprTestContainersConfig.java | 4 +--- .../src/main/java/io/dapr/testcontainers/DaprContainer.java | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java index 8ba41e9ef3..a9ca48bfbc 100644 --- a/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java +++ b/spring-boot-examples/workflows/src/test/java/io/dapr/springboot/examples/wfp/DaprTestContainersConfig.java @@ -55,9 +55,7 @@ public DaprContainer daprContainer(Network network) { .withAppPort(8080) .withNetwork(network) .withAppHealthCheckPath("/actuator/health") - .withAppChannelAddress("host.testcontainers.internal") - .withDaprLogLevel(DaprLogLevel.DEBUG) - .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())); + .withAppChannelAddress("host.testcontainers.internal"); } diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 3517e10490..1db758b531 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -84,8 +84,6 @@ public class DaprContainer extends GenericContainer { private boolean shouldReusePlacement; private boolean shouldReuseScheduler; - - /** * Creates a new Dapr container. * @@ -169,8 +167,6 @@ public DaprContainer withSchedulerService(String schedulerService) { return this; } - - public DaprContainer withAppName(String appName) { this.appName = appName; return this; From a45cb00f95e8c8c929db7d5d3fd664c0917c7bf3 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 16:34:05 -0500 Subject: [PATCH 19/33] reset appname for daprcontainer Signed-off-by: Cassandra Coyle --- .../main/java/io/dapr/testcontainers/DaprContainer.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 1db758b531..369d7c29f2 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -347,13 +347,9 @@ protected void configure() { public void start() { List cmds = new ArrayList<>(); cmds.add("./daprd"); - - String finalAppName = (appName != null && !appName.trim().isEmpty()) - ? appName - : "dapr-app-" + UUID.randomUUID().toString().substring(0, 8); - + cmds.add("--app-id"); - cmds.add(finalAppName); + cmds.add(appName); cmds.add("--dapr-listen-addresses=0.0.0.0"); From 14c91858dfd891f325c777a64ca3e794d636a229 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 16:34:48 -0500 Subject: [PATCH 20/33] reset empty line diff Signed-off-by: Cassandra Coyle --- .../src/main/java/io/dapr/testcontainers/DaprContainer.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 369d7c29f2..5cec995a33 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -347,12 +347,9 @@ protected void configure() { public void start() { List cmds = new ArrayList<>(); cmds.add("./daprd"); - cmds.add("--app-id"); cmds.add(appName); - cmds.add("--dapr-listen-addresses=0.0.0.0"); - cmds.add("--app-protocol"); cmds.add(DAPR_PROTOCOL.getName()); cmds.add("--placement-host-address"); From 6f42fed2a1c0969b7493c05a90a6cf9411eb92aa Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 16:39:40 -0500 Subject: [PATCH 21/33] rm constructor info from readme Signed-off-by: Cassandra Coyle --- .../java/io/dapr/examples/workflows/README.md | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/examples/src/main/java/io/dapr/examples/workflows/README.md b/examples/src/main/java/io/dapr/examples/workflows/README.md index 6aab2cbb8a..047312bd4b 100644 --- a/examples/src/main/java/io/dapr/examples/workflows/README.md +++ b/examples/src/main/java/io/dapr/examples/workflows/README.md @@ -768,24 +768,6 @@ public class App3FinalizeActivity implements WorkflowActivity { } ``` -**WorkflowTaskOptions Constructors for Cross-App Calls:** - -The `WorkflowTaskOptions` class supports several constructors for cross-app calls: - -```java -// App ID only -new WorkflowTaskOptions("app2") - -// Retry policy + app ID -new WorkflowTaskOptions(retryPolicy, "app2") - -// Retry handler + app ID -new WorkflowTaskOptions(retryHandler, "app2") - -// All parameters -new WorkflowTaskOptions(retryPolicy, retryHandler, "app2") -``` - **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 From 5dd363d6f8b2d7ddfdc9643fc9d0f31a3253058c Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Thu, 21 Aug 2025 16:43:05 -0500 Subject: [PATCH 22/33] debug -> info Signed-off-by: Cassandra Coyle --- .../springboot/examples/consumer/DaprTestContainersConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java index 0b810ad6cd..fb3d6b5479 100644 --- a/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java +++ b/spring-boot-examples/consumer-app/src/test/java/io/dapr/springboot/examples/consumer/DaprTestContainersConfig.java @@ -93,7 +93,7 @@ public DaprContainer daprContainer(Network daprNetwork, RabbitMQContainer rabbit .withAppName("consumer-app") .withNetwork(daprNetwork).withComponent(new Component("pubsub", "pubsub.rabbitmq", "v1", rabbitMqProperties)) - .withDaprLogLevel(DaprLogLevel.DEBUG) + .withDaprLogLevel(DaprLogLevel.INFO) .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) .withAppPort(8081).withAppChannelAddress("host.testcontainers.internal") .withReusablePlacement(reuse) From 3613cd383dadb57d7e50f40c654f0c777b81bdc7 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Fri, 22 Aug 2025 12:02:59 -0500 Subject: [PATCH 23/33] rm super.start Signed-off-by: Cassandra Coyle --- .../main/java/io/dapr/testcontainers/DaprContainer.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 5cec995a33..5c5e8fd715 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -341,10 +341,7 @@ protected void configure() { } dependsOn(placementContainer, schedulerContainer); - } - @Override - public void start() { List cmds = new ArrayList<>(); cmds.add("./daprd"); cmds.add("--app-id"); @@ -398,10 +395,6 @@ public void start() { LOGGER.info("\t" + Arrays.toString(cmdArray) + "\n"); withCommand(cmdArray); - - - - super.start(); } public String getAppName() { From 1aaefa64699e484c48f3c038f9f0d71342a23b09 Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Fri, 22 Aug 2025 14:33:45 -0500 Subject: [PATCH 24/33] reset dapr container diff Signed-off-by: Cassandra Coyle --- .../io/dapr/testcontainers/DaprContainer.java | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index 5c5e8fd715..51dce6f076 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -39,7 +39,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import static io.dapr.testcontainers.DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG; import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG; @@ -288,42 +287,6 @@ protected void configure() { withNetwork(Network.newNetwork()); } - if (configuration != null) { - String configurationYaml = CONFIGURATION_CONVERTER.convert(configuration); - - LOGGER.info("> Configuration YAML: \n"); - LOGGER.info("\t\n" + configurationYaml + "\n"); - - withCopyToContainer(Transferable.of(configurationYaml), "/dapr-resources/" + configuration.getName() + ".yaml"); - } - - for (Component component : components) { - String componentYaml = COMPONENT_CONVERTER.convert(component); - - LOGGER.info("> Component YAML: \n"); - LOGGER.info("\t\n" + componentYaml + "\n"); - - withCopyToContainer(Transferable.of(componentYaml), "/dapr-resources/" + component.getName() + ".yaml"); - } - - for (Subscription subscription : subscriptions) { - String subscriptionYaml = SUBSCRIPTION_CONVERTER.convert(subscription); - - LOGGER.info("> Subscription YAML: \n"); - LOGGER.info("\t\n" + subscriptionYaml + "\n"); - - withCopyToContainer(Transferable.of(subscriptionYaml), "/dapr-resources/" + subscription.getName() + ".yaml"); - } - - for (HttpEndpoint endpoint : httpEndpoints) { - String endpointYaml = HTTPENDPOINT_CONVERTER.convert(endpoint); - - LOGGER.info("> HTTPEndpoint YAML: \n"); - LOGGER.info("\t\n" + endpointYaml + "\n"); - - withCopyToContainer(Transferable.of(endpointYaml), "/dapr-resources/" + endpoint.getName() + ".yaml"); - } - if (this.placementContainer == null) { this.placementContainer = new DaprPlacementContainer(this.placementDockerImageName) .withNetwork(getNetwork()) @@ -340,8 +303,6 @@ protected void configure() { this.schedulerContainer.start(); } - dependsOn(placementContainer, schedulerContainer); - List cmds = new ArrayList<>(); cmds.add("./daprd"); cmds.add("--app-id"); @@ -395,6 +356,53 @@ protected void configure() { LOGGER.info("\t" + Arrays.toString(cmdArray) + "\n"); withCommand(cmdArray); + + if (configuration != null) { + String configurationYaml = CONFIGURATION_CONVERTER.convert(configuration); + + LOGGER.info("> Configuration YAML: \n"); + LOGGER.info("\t\n" + configurationYaml + "\n"); + + withCopyToContainer(Transferable.of(configurationYaml), "/dapr-resources/" + configuration.getName() + ".yaml"); + } + + if (components.isEmpty()) { + components.add(new Component("kvstore", "state.in-memory", "v1", Collections.emptyMap())); + components.add(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())); + } + + if (subscriptions.isEmpty() && !components.isEmpty()) { + subscriptions.add(new Subscription("local", "pubsub", "topic", "/events")); + } + + for (Component component : components) { + String componentYaml = COMPONENT_CONVERTER.convert(component); + + LOGGER.info("> Component YAML: \n"); + LOGGER.info("\t\n" + componentYaml + "\n"); + + withCopyToContainer(Transferable.of(componentYaml), "/dapr-resources/" + component.getName() + ".yaml"); + } + + for (Subscription subscription : subscriptions) { + String subscriptionYaml = SUBSCRIPTION_CONVERTER.convert(subscription); + + LOGGER.info("> Subscription YAML: \n"); + LOGGER.info("\t\n" + subscriptionYaml + "\n"); + + withCopyToContainer(Transferable.of(subscriptionYaml), "/dapr-resources/" + subscription.getName() + ".yaml"); + } + + for (HttpEndpoint endpoint : httpEndpoints) { + String endpointYaml = HTTPENDPOINT_CONVERTER.convert(endpoint); + + LOGGER.info("> HTTPEndpoint YAML: \n"); + LOGGER.info("\t\n" + endpointYaml + "\n"); + + withCopyToContainer(Transferable.of(endpointYaml), "/dapr-resources/" + endpoint.getName() + ".yaml"); + } + + dependsOn(placementContainer, schedulerContainer); } public String getAppName() { From 4e9fab4ffd093b2745caa24d1317b3b5c2ae023b Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Tue, 26 Aug 2025 10:41:12 -0400 Subject: [PATCH 25/33] add test for codecov Signed-off-by: Cassandra Coyle --- .../workflows/WorkflowTaskOptionsTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java new file mode 100644 index 0000000000..5ef82ecf78 --- /dev/null +++ b/sdk-workflows/src/test/java/io/dapr/workflows/WorkflowTaskOptionsTest.java @@ -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()); + } +} From c1aadc137ba87a0da8ee96399f65b5688088909f Mon Sep 17 00:00:00 2001 From: Cassandra Coyle Date: Tue, 26 Aug 2025 10:47:36 -0400 Subject: [PATCH 26/33] up timeout time to unblock PR Signed-off-by: Cassandra Coyle --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7bfc93568d..fd37a28ead 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: build: name: "Build jdk:${{ matrix.java }} sb:${{ matrix.spring-boot-display-version }} exp:${{ matrix.experimental }}" runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 60 continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false From 973ed3c3452f5ae12c648499c48ec6c0b2ca76db Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Mon, 1 Sep 2025 17:45:26 +0200 Subject: [PATCH 27/33] deps: Update durabletask-client to 1.5.10 Signed-off-by: Javier Aliaga --- sdk-workflows/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index 525f6a6c20..3792008268 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -47,7 +47,7 @@ io.dapr durabletask-client - 1.5.7 + 1.5.10