diff --git a/daprdocs/content/en/java-sdk-docs/java-workflow/java-workflow-howto.md b/daprdocs/content/en/java-sdk-docs/java-workflow/java-workflow-howto.md
index 1c12aa50c2..ccc365cf42 100644
--- a/daprdocs/content/en/java-sdk-docs/java-workflow/java-workflow-howto.md
+++ b/daprdocs/content/en/java-sdk-docs/java-workflow/java-workflow-howto.md
@@ -6,7 +6,7 @@ weight: 20000
description: How to get up and running with workflows using the Dapr Java SDK
---
-Let’s create a Dapr workflow and invoke it using the console. With the [provided workflow example](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/workflows), you will:
+Let's create a Dapr workflow and invoke it using the console. With the [provided workflow example](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/workflows), you will:
- Execute the workflow instance using the [Java workflow worker](https://github.com/dapr/java-sdk/blob/master/examples/src/main/java/io/dapr/examples/workflows/DemoWorkflowWorker.java)
- Utilize the Java workflow client and API calls to [start and terminate workflow instances](https://github.com/dapr/java-sdk/blob/master/examples/src/main/java/io/dapr/examples/workflows/DemoWorkflowClient.java)
@@ -85,11 +85,10 @@ You're up and running! Both Dapr and your app logs will appear here.
== APP == INFO: Durable Task worker is connecting to sidecar at 127.0.0.1:50001.
```
-## Run the `DemoWorkflowClient
+## Run the `DemoWorkflowClient`
The `DemoWorkflowClient` starts instances of workflows that have been registered with Dapr.
-
```java
public class DemoWorkflowClient {
@@ -246,4 +245,40 @@ Exiting DemoWorkflowClient.
## Next steps
- [Learn more about Dapr workflow]({{% ref workflow-overview.md %}})
-- [Workflow API reference]({{% ref workflow_api.md %}})
\ No newline at end of file
+- [Workflow API reference]({{% ref workflow_api.md %}})
+
+## Advanced features
+
+### Task Execution Keys
+
+Task execution keys are unique identifiers generated by the durabletask-java library. They are stored in the `WorkflowActivityContext` and can be used to track and manage the execution of workflow activities. They are particularly useful for:
+
+1. **Idempotency**: Ensuring activities are not executed multiple times for the same task
+2. **State Management**: Tracking the state of activity execution
+3. **Error Handling**: Managing retries and failures in a controlled manner
+
+Here's an example of how to use task execution keys in your workflow activities:
+
+```java
+public class TaskExecutionKeyActivity implements WorkflowActivity {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ // Get the task execution key for this activity
+ String taskExecutionKey = ctx.getTaskExecutionKey();
+
+ // Use the key to implement idempotency or state management
+ // For example, check if this task has already been executed
+ if (isTaskAlreadyExecuted(taskExecutionKey)) {
+ return getPreviousResult(taskExecutionKey);
+ }
+
+ // Execute the activity logic
+ Object result = executeActivityLogic();
+
+ // Store the result with the task execution key
+ storeResult(taskExecutionKey, result);
+
+ return result;
+ }
+}
+```
diff --git a/pom.xml b/pom.xml
index 6ea7d1e51a..3220fc4fe8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,7 @@
2.0
1.21.3
- 3.4.6
+ 3.4.9
6.2.7
1.7.0
@@ -65,6 +65,8 @@
2.14.0
3.4.0
0.3.1
+ 1.26.0
+ 1.17.0
@@ -372,6 +374,17 @@
wiremock-standalone
${wiremock.version}
+
+ org.apache.commons
+ commons-compress
+ ${commons-compress.version}
+
+
+ commons-codec
+ commons-codec
+ ${commons-codec.version}
+ testf
+
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java
index 48f727b6f1..db531d5146 100644
--- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/DaprWorkflowsIT.java
@@ -15,6 +15,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+
import io.dapr.testcontainers.Component;
import io.dapr.testcontainers.DaprContainer;
import io.dapr.testcontainers.DaprLogLevel;
@@ -41,6 +42,7 @@
import java.util.Map;
import static io.dapr.it.testcontainers.ContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
+import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -153,7 +155,7 @@ public void testNamedActivitiesWorkflows() throws Exception {
String instanceId = workflowClient.scheduleNewWorkflow(TestNamedActivitiesWorkflow.class, payload);
workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(10), false);
-
+
Duration timeout = Duration.ofSeconds(10);
WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, timeout, true);
@@ -171,6 +173,28 @@ public void testNamedActivitiesWorkflows() throws Exception {
assertEquals(instanceId, workflowOutput.getWorkflowId());
}
+ @Test
+ public void testExecutionKeyWorkflows() throws Exception {
+ TestWorkflowPayload payload = new TestWorkflowPayload(new ArrayList<>());
+ String instanceId = workflowClient.scheduleNewWorkflow(TestExecutionKeysWorkflow.class, payload);
+
+ workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(100), false);
+
+ Duration timeout = Duration.ofSeconds(1000);
+ WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, timeout, true);
+
+ assertNotNull(workflowStatus);
+
+ TestWorkflowPayload workflowOutput = deserialize(workflowStatus.getSerializedOutput());
+
+ assertEquals(1, workflowOutput.getPayloads().size());
+ assertEquals("Execution key found", workflowOutput.getPayloads().get(0));
+
+ assertTrue(KeyStore.getInstance().size() == 1);
+
+ assertEquals(instanceId, workflowOutput.getWorkflowId());
+ }
+
private TestWorkflowPayload deserialize(String value) throws JsonProcessingException {
return OBJECT_MAPPER.readValue(value, TestWorkflowPayload.class);
}
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/KeyStore.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/KeyStore.java
new file mode 100644
index 0000000000..1e3f95aae0
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/KeyStore.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2025 The Dapr Authors
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package io.dapr.it.testcontainers.workflows;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class KeyStore {
+
+ private final Map keyStore = new HashMap<>();
+
+ private static KeyStore instance;
+
+ private KeyStore() {
+ }
+
+ public static KeyStore getInstance() {
+ if (instance == null) {
+ synchronized (KeyStore.class) {
+ if (instance == null) {
+ instance = new KeyStore();
+ }
+ }
+ }
+ return instance;
+ }
+
+
+ public void addKey(String key, Boolean value) {
+ keyStore.put(key, value);
+ }
+
+ public Boolean getKey(String key) {
+ return keyStore.get(key);
+ }
+
+ public void removeKey(String key) {
+ keyStore.remove(key);
+ }
+
+ public int size() {
+ return keyStore.size();
+ }
+
+}
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TaskExecutionIdActivity.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TaskExecutionIdActivity.java
new file mode 100644
index 0000000000..27acb03aeb
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TaskExecutionIdActivity.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+import io.dapr.workflows.WorkflowActivity;
+import io.dapr.workflows.WorkflowActivityContext;
+
+public class TaskExecutionIdActivity implements WorkflowActivity {
+
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class);
+ KeyStore keyStore = KeyStore.getInstance();
+ Boolean exists = keyStore.getKey(ctx.getTaskExecutionId());
+ if (!Boolean.TRUE.equals(exists)) {
+ keyStore.addKey(ctx.getTaskExecutionId(), true);
+ workflowPayload.getPayloads().add("Execution key not found");
+ throw new IllegalStateException("Task execution key not found");
+ }
+ workflowPayload.getPayloads().add("Execution key found");
+ return workflowPayload;
+ }
+
+}
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestExecutionKeysWorkflow.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestExecutionKeysWorkflow.java
new file mode 100644
index 0000000000..65eb1047c4
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestExecutionKeysWorkflow.java
@@ -0,0 +1,58 @@
+/*
+ * 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;
+
+import io.dapr.durabletask.Task;
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import io.dapr.workflows.WorkflowTaskOptions;
+import io.dapr.workflows.WorkflowTaskRetryPolicy;
+
+import java.time.Duration;
+
+import org.slf4j.Logger;
+
+public class TestExecutionKeysWorkflow implements Workflow {
+
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+
+ Logger logger = ctx.getLogger();
+ String instanceId = ctx.getInstanceId();
+ logger.info("Starting Workflow: " + ctx.getName());
+ logger.info("Instance ID: " + instanceId);
+ logger.info("Current Orchestration Time: " + ctx.getCurrentInstant());
+
+ TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class);
+ workflowPayload.setWorkflowId(instanceId);
+
+ WorkflowTaskOptions options = new WorkflowTaskOptions(WorkflowTaskRetryPolicy.newBuilder()
+ .setMaxNumberOfAttempts(3)
+ .setFirstRetryInterval(Duration.ofSeconds(1))
+ .setMaxRetryInterval(Duration.ofSeconds(10))
+ .setBackoffCoefficient(2.0)
+ .setRetryTimeout(Duration.ofSeconds(50))
+ .build());
+
+
+ Task t = ctx.callActivity(TaskExecutionIdActivity.class.getName(), workflowPayload, options,TestWorkflowPayload.class);
+
+ TestWorkflowPayload payloadAfterExecution = t.await();
+
+ ctx.complete(payloadAfterExecution);
+ };
+ }
+
+}
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestWorkflowsConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestWorkflowsConfiguration.java
index 2f6801a773..3ba555825e 100644
--- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestWorkflowsConfiguration.java
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/TestWorkflowsConfiguration.java
@@ -50,40 +50,41 @@ public WorkflowRuntimeBuilder workflowRuntimeBuilder(
@Value("${dapr.http.endpoint}") String daprHttpEndpoint,
@Value("${dapr.grpc.endpoint}") String daprGrpcEndpoint
){
- Map overrides = Map.of(
- "dapr.http.endpoint", daprHttpEndpoint,
- "dapr.grpc.endpoint", daprGrpcEndpoint
- );
-
- WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(new Properties(overrides));
+ Map overrides = Map.of(
+ "dapr.http.endpoint", daprHttpEndpoint,
+ "dapr.grpc.endpoint", daprGrpcEndpoint
+ );
- builder.registerWorkflow(TestWorkflow.class);
- builder.registerWorkflow(TestNamedActivitiesWorkflow.class);
+ WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(new Properties(overrides));
- builder.registerActivity(FirstActivity.class);
- builder.registerActivity(SecondActivity.class);
- builder.registerActivity("a",FirstActivity.class);
- builder.registerActivity("b",FirstActivity.class);
- builder.registerActivity("c", new SecondActivity());
- builder.registerActivity("d", new WorkflowActivity() {
- @Override
- public Object run(WorkflowActivityContext ctx) {
- TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class);
- workflowPayload.getPayloads().add("Anonymous Activity");
- return workflowPayload;
- }
- });
- builder.registerActivity("e", new WorkflowActivity() {
- @Override
- public Object run(WorkflowActivityContext ctx) {
- TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class);
- workflowPayload.getPayloads().add("Anonymous Activity 2");
- return workflowPayload;
- }
- });
+ builder.registerWorkflow(TestWorkflow.class);
+ builder.registerWorkflow(TestExecutionKeysWorkflow.class);
+ builder.registerWorkflow(TestNamedActivitiesWorkflow.class);
+ builder.registerActivity(FirstActivity.class);
+ builder.registerActivity(SecondActivity.class);
+ builder.registerActivity(TaskExecutionIdActivity.class);
+ builder.registerActivity("a", FirstActivity.class);
+ builder.registerActivity("b", FirstActivity.class);
+ builder.registerActivity("c", new SecondActivity());
+ builder.registerActivity("d", new WorkflowActivity() {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class);
+ workflowPayload.getPayloads().add("Anonymous Activity");
+ return workflowPayload;
+ }
+ });
+ builder.registerActivity("e", new WorkflowActivity() {
+ @Override
+ public Object run(WorkflowActivityContext ctx) {
+ TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class);
+ workflowPayload.getPayloads().add("Anonymous Activity 2");
+ return workflowPayload;
+ }
+ });
- return builder;
+ return builder;
}
}
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java
index 3fe5d88a23..dedde8901b 100644
--- a/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/WorkflowActivityContext.java
@@ -17,6 +17,8 @@ public interface WorkflowActivityContext {
String getName();
+ String getTaskExecutionId();
+
T getInput(Class targetType);
}
diff --git a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java
index 551c21a373..8de4f7e747 100644
--- a/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java
+++ b/sdk-workflows/src/main/java/io/dapr/workflows/runtime/DefaultWorkflowActivityContext.java
@@ -56,4 +56,9 @@ public String getName() {
public T getInput(Class targetType) {
return this.innerContext.getInput(targetType);
}
+
+ @Override
+ public String getTaskExecutionId() {
+ return this.innerContext.getTaskExecutionId();
+ }
}
diff --git a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java
index f42a1d651a..377a71f98f 100644
--- a/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java
+++ b/sdk-workflows/src/test/java/io/dapr/workflows/runtime/WorkflowActivityClassWrapperTest.java
@@ -13,7 +13,7 @@ public static class TestActivity implements WorkflowActivity {
@Override
public Object run(WorkflowActivityContext ctx) {
String activityContextName = ctx.getName();
- return ctx.getInput(String.class) + " world! from " + activityContextName;
+ return ctx.getInput(String.class) + " world! from " + activityContextName + " with task execution key " + ctx.getTaskExecutionId();
}
}
@@ -34,10 +34,11 @@ public void createWithClass() {
when(mockContext.getInput(String.class)).thenReturn("Hello");
when(mockContext.getName()).thenReturn("TestActivityContext");
+ when(mockContext.getTaskExecutionId()).thenReturn("123");
Object result = wrapper.create().run(mockContext);
verify(mockContext, times(1)).getInput(String.class);
- assertEquals("Hello world! from TestActivityContext", result);
+ assertEquals("Hello world! from TestActivityContext with task execution key 123", result);
}
}
diff --git a/testcontainers-dapr/pom.xml b/testcontainers-dapr/pom.xml
index 429306524b..716b7853c9 100644
--- a/testcontainers-dapr/pom.xml
+++ b/testcontainers-dapr/pom.xml
@@ -33,6 +33,14 @@
org.testcontainers
testcontainers
+
+ commons-codec
+ commons-codec
+
+
+ org.apache.commons
+ commons-compress
+