orchestrationFactories = new HashMap<>();
- orchestrationFactories.put("*", new TaskOrchestrationFactory() {
+ TaskOrchestrationFactories orchestrationFactories = new TaskOrchestrationFactories();
+ orchestrationFactories.addOrchestration(new TaskOrchestrationFactory() {
@Override
public String getName() {
return "*";
@@ -145,6 +146,16 @@ public String getName() {
public TaskOrchestration create() {
return orchestration;
}
+
+ @Override
+ public String getVersionName() {
+ return "";
+ }
+
+ @Override
+ public Boolean isLatestVersion() {
+ return false;
+ }
});
TaskOrchestrationExecutor taskOrchestrationExecutor = new TaskOrchestrationExecutor(
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java b/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java
index 1bdd33ab38..e9530ae815 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/OrchestrationRuntimeStatus.java
@@ -68,7 +68,12 @@ public enum OrchestrationRuntimeStatus {
/**
* The orchestration is in a suspended state.
*/
- SUSPENDED;
+ SUSPENDED,
+
+ /**
+ * The orchestration is in a stalled state.
+ */
+ STALLED;
static OrchestrationRuntimeStatus fromProtobuf(OrchestratorService.OrchestrationStatus status) {
switch (status) {
@@ -88,6 +93,8 @@ static OrchestrationRuntimeStatus fromProtobuf(OrchestratorService.Orchestration
return PENDING;
case ORCHESTRATION_STATUS_SUSPENDED:
return SUSPENDED;
+ case ORCHESTRATION_STATUS_STALLED:
+ return STALLED;
default:
throw new IllegalArgumentException(String.format("Unknown status value: %s", status));
}
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/Task.java b/durabletask-client/src/main/java/io/dapr/durabletask/Task.java
index a3f3313816..de2f13e871 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/Task.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/Task.java
@@ -27,12 +27,14 @@
*
* Task{@literal <}int{@literal >} activityTask = ctx.callActivity("MyActivity", someInput, int.class);
*
+ *
* Orchestrator code uses the {@link #await()} method to block on the completion of the task and retrieve the result.
* If the task is not yet complete, the {@code await()} method will throw an {@link OrchestratorBlockedException}, which
* pauses the orchestrator's execution so that it can save its progress into durable storage and schedule any
* outstanding work. When the task is complete, the orchestrator will run again from the beginning and the next time
* the task's {@code await()} method is called, the result will be returned, or a {@link TaskFailedException} will be
* thrown if the result of the task was an unhandled exception.
+ *
* Note that orchestrator code must never catch {@code OrchestratorBlockedException} because doing so can cause the
* orchestration instance to get permanently stuck.
*
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityContext.java b/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityContext.java
index b2043b51ee..7a0d1ed1ee 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityContext.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/TaskActivityContext.java
@@ -34,7 +34,6 @@ public interface TaskActivityContext {
*/
T getInput(Class targetType);
-
/**
* Gets the execution id of the current task activity.
*
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskFailedException.java b/durabletask-client/src/main/java/io/dapr/durabletask/TaskFailedException.java
index 377eecb426..5362e830c7 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskFailedException.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/TaskFailedException.java
@@ -16,6 +16,7 @@
/**
* Exception that gets thrown when awaiting a {@link Task} for an activity or sub-orchestration that fails with an
* unhandled exception.
+ *
* Detailed information associated with a particular task failure can be retrieved
* using the {@link #getErrorDetails()} method.
*/
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java
index df0c95ec82..97d851b47f 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationContext.java
@@ -352,6 +352,15 @@ default void continueAsNew(Object input) {
*/
void continueAsNew(Object input, boolean preserveUnprocessedEvents);
+ /**
+ * Check if the given patch name can be applied to the orchestration.
+ *
+ * @param patchName The name of the patch to check.
+ * @return True if the given patch name can be applied to the orchestration, False otherwise.
+ */
+
+ boolean isPatched(String patchName);
+
/**
* Create a new Uuid that is safe for replay within an orchestration or operation.
*
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java
index 7a3436b036..6096c9bdf9 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationExecutor.java
@@ -19,7 +19,11 @@
import io.dapr.durabletask.implementation.protobuf.OrchestratorService.ScheduleTaskAction.Builder;
import io.dapr.durabletask.interruption.ContinueAsNewInterruption;
import io.dapr.durabletask.interruption.OrchestratorBlockedException;
+import io.dapr.durabletask.orchestration.TaskOrchestrationFactories;
+import io.dapr.durabletask.orchestration.TaskOrchestrationFactory;
+import io.dapr.durabletask.orchestration.exception.VersionNotRegisteredException;
import io.dapr.durabletask.util.UuidGenerator;
+import org.apache.commons.lang3.StringUtils;
import javax.annotation.Nullable;
import java.time.Duration;
@@ -47,14 +51,14 @@
final class TaskOrchestrationExecutor {
private static final String EMPTY_STRING = "";
- private final HashMap orchestrationFactories;
+ private final TaskOrchestrationFactories orchestrationFactories;
private final DataConverter dataConverter;
private final Logger logger;
private final Duration maximumTimerInterval;
private final String appId;
public TaskOrchestrationExecutor(
- HashMap orchestrationFactories,
+ TaskOrchestrationFactories orchestrationFactories,
DataConverter dataConverter,
Duration maximumTimerInterval,
Logger logger,
@@ -79,6 +83,9 @@ public TaskOrchestratorResult execute(List pas
}
completed = true;
logger.finest("The orchestrator execution completed normally");
+ } catch (VersionNotRegisteredException versionNotRegisteredException) {
+ logger.warning("The orchestrator version is not registered: " + versionNotRegisteredException.toString());
+ context.setVersionNotRegistered();
} catch (OrchestratorBlockedException orchestratorBlockedException) {
logger.fine("The orchestrator has yielded and will await for new events.");
} catch (ContinueAsNewInterruption continueAsNewInterruption) {
@@ -87,7 +94,7 @@ public TaskOrchestratorResult execute(List pas
} catch (Exception e) {
// The orchestrator threw an unhandled exception - fail it
// TODO: What's the right way to log this?
- logger.warning("The orchestrator failed with an unhandled exception: " + e.toString());
+ logger.warning("The orchestrator failed with an unhandled exception: " + e);
context.fail(new FailureDetails(e));
}
@@ -97,12 +104,16 @@ public TaskOrchestratorResult execute(List pas
context.complete(null);
}
- return new TaskOrchestratorResult(context.pendingActions.values(), context.getCustomStatus());
+ return new TaskOrchestratorResult(context.pendingActions.values(),
+ context.getCustomStatus(),
+ context.versionName,
+ context.encounteredPatches);
}
private class ContextImplTask implements TaskOrchestrationContext {
private String orchestratorName;
+ private final List encounteredPatches = new ArrayList<>();
private String rawInput;
private String instanceId;
private Instant currentInstant;
@@ -127,6 +138,12 @@ private class ContextImplTask implements TaskOrchestrationContext {
private Object continuedAsNewInput;
private boolean preserveUnprocessedEvents;
private Object customStatus;
+ private final Map appliedPatches = new HashMap<>();
+ private final Map historyPatches = new HashMap<>();
+
+ private String orchestratorVersionName;
+
+ private String versionName;
public ContextImplTask(List pastEvents,
List newEvents) {
@@ -363,6 +380,34 @@ public Task callActivity(
return this.createAppropriateTask(taskFactory, options);
}
+ @Override
+ public boolean isPatched(String patchName) {
+ var isPatched = this.checkPatch(patchName);
+ if (isPatched) {
+ this.encounteredPatches.add(patchName);
+ }
+
+ return isPatched;
+ }
+
+ public boolean checkPatch(String patchName) {
+ if (this.appliedPatches.containsKey(patchName)) {
+ return this.appliedPatches.get(patchName);
+ }
+
+ if (this.historyPatches.containsKey(patchName)) {
+ this.appliedPatches.put(patchName, true);
+ return true;
+ }
+
+ if (this.isReplaying) {
+ this.appliedPatches.put(patchName, false);
+ return false;
+ }
+ this.appliedPatches.put(patchName, true);
+ return true;
+ }
+
@Override
public void continueAsNew(Object input, boolean preserveUnprocessedEvents) {
Helpers.throwIfOrchestratorComplete(this.isComplete);
@@ -438,7 +483,7 @@ public Task callSubOrchestrator(
if (input instanceof TaskOptions) {
throw new IllegalArgumentException("TaskOptions cannot be used as an input. "
- + "Did you call the wrong method overload?");
+ + "Did you call the wrong method overload?");
}
String serializedInput = this.dataConverter.serialize(input);
@@ -924,6 +969,14 @@ private void processEvent(OrchestratorService.HistoryEvent e) {
case ORCHESTRATORSTARTED:
Instant instant = DataConverter.getInstantFromTimestamp(e.getTimestamp());
this.setCurrentInstant(instant);
+
+ if (StringUtils.isNotEmpty(e.getOrchestratorStarted().getVersion().getName())) {
+ this.orchestratorVersionName = e.getOrchestratorStarted().getVersion().getName();
+ }
+ for (var patch : e.getOrchestratorStarted().getVersion().getPatchesList()) {
+ this.historyPatches.put(patch, true);
+ }
+
this.logger.fine(() -> this.instanceId + ": Workflow orchestrator started");
break;
case ORCHESTRATORCOMPLETED:
@@ -938,18 +991,27 @@ private void processEvent(OrchestratorService.HistoryEvent e) {
this.logger.fine(() -> this.instanceId + ": Workflow execution started");
this.setAppId(e.getRouter().getSourceAppID());
+ var versionName = "";
+ if (!StringUtils.isEmpty(this.orchestratorVersionName)) {
+ versionName = this.orchestratorVersionName;
+ }
+
// Create and invoke the workflow orchestrator
TaskOrchestrationFactory factory = TaskOrchestrationExecutor.this.orchestrationFactories
- .get(executionStarted.getName());
+ .getOrchestrationFactory(executionStarted.getName(), versionName);
+
if (factory == null) {
// Try getting the default orchestrator
- factory = TaskOrchestrationExecutor.this.orchestrationFactories.get("*");
+ factory = TaskOrchestrationExecutor.this.orchestrationFactories
+ .getOrchestrationFactory("*");
}
// TODO: Throw if the factory is null (orchestration by that name doesn't exist)
if (factory == null) {
throw new IllegalStateException("No factory found for orchestrator: " + executionStarted.getName());
}
+ this.versionName = factory.getVersionName();
+
TaskOrchestration orchestrator = factory.create();
orchestrator.run(this);
break;
@@ -959,6 +1021,9 @@ private void processEvent(OrchestratorService.HistoryEvent e) {
case EXECUTIONTERMINATED:
this.handleExecutionTerminated(e);
break;
+ case EXECUTIONSTALLED:
+ this.logger.fine(() -> this.instanceId + ": Workflow execution stalled");
+ break;
case TASKSCHEDULED:
this.handleTaskScheduled(e);
break;
@@ -998,6 +1063,22 @@ private void processEvent(OrchestratorService.HistoryEvent e) {
}
}
+ public void setVersionNotRegistered() {
+ this.pendingActions.clear();
+
+ OrchestratorService.CompleteOrchestrationAction.Builder builder = OrchestratorService.CompleteOrchestrationAction
+ .newBuilder();
+ builder.setOrchestrationStatus(OrchestratorService.OrchestrationStatus.ORCHESTRATION_STATUS_STALLED);
+
+ int id = this.sequenceNumber++;
+ OrchestratorService.OrchestratorAction action = OrchestratorService.OrchestratorAction.newBuilder()
+ .setId(id)
+ .setCompleteOrchestration(builder.build())
+ .build();
+ this.pendingActions.put(id, action);
+
+ }
+
private class TaskRecord {
private final CompletableTask task;
private final String taskName;
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestratorResult.java b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestratorResult.java
index 705a41d5c0..8243176031 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestratorResult.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestratorResult.java
@@ -17,6 +17,7 @@
import java.util.Collection;
import java.util.Collections;
+import java.util.List;
final class TaskOrchestratorResult {
@@ -24,10 +25,16 @@ final class TaskOrchestratorResult {
private final String customStatus;
- public TaskOrchestratorResult(Collection actions, String customStatus) {
+ private final String version;
+
+ private final List patches;
+
+ public TaskOrchestratorResult(Collection actions,
+ String customStatus, String version, List patches) {
this.actions = Collections.unmodifiableCollection(actions);
- ;
this.customStatus = customStatus;
+ this.version = version;
+ this.patches = patches;
}
public Collection getActions() {
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactories.java b/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactories.java
new file mode 100644
index 0000000000..c513b4f1f5
--- /dev/null
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactories.java
@@ -0,0 +1,117 @@
+/*
+ * 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.durabletask.orchestration;
+
+import io.dapr.durabletask.orchestration.exception.VersionNotRegisteredException;
+
+import java.util.HashMap;
+import java.util.logging.Logger;
+
+public class TaskOrchestrationFactories {
+ private static final Logger logger = Logger.getLogger(TaskOrchestrationFactories.class.getPackage().getName());
+
+ final HashMap orchestrationFactories = new HashMap<>();
+ final HashMap> versionedOrchestrationFactories = new HashMap<>();
+ final HashMap latestVersionOrchestrationFactories = new HashMap<>();
+
+ /**
+ * Adds a new orchestration factory to the registry.
+ *
+ * @param factory the factory to add
+ */
+ public void addOrchestration(TaskOrchestrationFactory factory) {
+ String key = factory.getName();
+ if (this.emptyString(key)) {
+ throw new IllegalArgumentException("A non-empty task orchestration name is required.");
+ }
+
+ if (this.orchestrationFactories.containsKey(key)) {
+ throw new IllegalArgumentException(
+ String.format("A task orchestration factory named %s is already registered.", key));
+ }
+
+ if (emptyString(factory.getVersionName())) {
+ this.orchestrationFactories.put(key, factory);
+ return;
+ }
+
+ if (!this.versionedOrchestrationFactories.containsKey(key)) {
+ this.versionedOrchestrationFactories.put(key, new HashMap<>());
+ }
+
+ if (this.versionedOrchestrationFactories.get(key).containsKey(factory.getVersionName())) {
+ throw new IllegalArgumentException("The version name " + factory.getVersionName() + "for "
+ + factory.getName() + " is already registered.");
+ }
+
+ this.versionedOrchestrationFactories.get(key).put(factory.getVersionName(), factory);
+
+ if (factory.isLatestVersion()) {
+ logger.info("Setting latest version for " + key + " to " + factory.getVersionName());
+ this.latestVersionOrchestrationFactories.put(key, factory.getVersionName());
+ }
+
+ }
+
+ /**
+ * Gets the orchestration factory for the specified orchestration name.
+ *
+ * @param orchestrationName the orchestration name
+ * @return the orchestration factory
+ */
+ public TaskOrchestrationFactory getOrchestrationFactory(String orchestrationName) {
+ logger.info("Get orchestration factory for " + orchestrationName);
+ if (this.orchestrationFactories.containsKey(orchestrationName)) {
+ return this.orchestrationFactories.get(orchestrationName);
+ }
+
+ return this.getOrchestrationFactory(orchestrationName, "");
+ }
+
+ /**
+ * Gets the orchestration factory for the specified orchestration name and version.
+ *
+ * @param orchestrationName the orchestration name
+ * @param versionName the version name
+ * @return the orchestration factory
+ */
+ public TaskOrchestrationFactory getOrchestrationFactory(String orchestrationName, String versionName) {
+ logger.info("Get orchestration factory for " + orchestrationName + " version " + versionName);
+ if (this.orchestrationFactories.containsKey(orchestrationName)) {
+ return this.orchestrationFactories.get(orchestrationName);
+ }
+
+ if (!this.versionedOrchestrationFactories.containsKey(orchestrationName)) {
+ logger.warning("No orchestration factory registered for " + orchestrationName);
+ return null;
+ }
+
+ if (this.emptyString(versionName)) {
+ logger.info("No version specified, returning latest version");
+ String latestVersion = this.latestVersionOrchestrationFactories.get(orchestrationName);
+ logger.info("Latest version is " + latestVersion);
+ return this.versionedOrchestrationFactories.get(orchestrationName).get(latestVersion);
+ }
+
+ if (this.versionedOrchestrationFactories.get(orchestrationName).containsKey(versionName)) {
+ return this.versionedOrchestrationFactories.get(orchestrationName).get(versionName);
+ }
+
+ throw new VersionNotRegisteredException();
+ }
+
+ private boolean emptyString(String s) {
+ return s == null || s.isEmpty();
+ }
+}
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationFactory.java b/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactory.java
similarity index 87%
rename from durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationFactory.java
rename to durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactory.java
index 274813b69f..a5e1b6a3cf 100644
--- a/durabletask-client/src/main/java/io/dapr/durabletask/TaskOrchestrationFactory.java
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/TaskOrchestrationFactory.java
@@ -11,7 +11,9 @@
limitations under the License.
*/
-package io.dapr.durabletask;
+package io.dapr.durabletask.orchestration;
+
+import io.dapr.durabletask.TaskOrchestration;
/**
* Factory interface for producing {@link TaskOrchestration} implementations.
@@ -30,4 +32,8 @@ public interface TaskOrchestrationFactory {
* @return the created orchestration instance
*/
TaskOrchestration create();
+
+ String getVersionName();
+
+ Boolean isLatestVersion();
}
diff --git a/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/exception/VersionNotRegisteredException.java b/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/exception/VersionNotRegisteredException.java
new file mode 100644
index 0000000000..f69ad9ea65
--- /dev/null
+++ b/durabletask-client/src/main/java/io/dapr/durabletask/orchestration/exception/VersionNotRegisteredException.java
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2026 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.durabletask.orchestration.exception;
+
+public class VersionNotRegisteredException extends RuntimeException {
+}
diff --git a/durabletask-client/src/test/java/io/dapr/durabletask/IntegrationTestBase.java b/durabletask-client/src/test/java/io/dapr/durabletask/IntegrationTestBase.java
index bbfcde0469..d0a8a8faa6 100644
--- a/durabletask-client/src/test/java/io/dapr/durabletask/IntegrationTestBase.java
+++ b/durabletask-client/src/test/java/io/dapr/durabletask/IntegrationTestBase.java
@@ -13,6 +13,7 @@
package io.dapr.durabletask;
+import io.dapr.durabletask.orchestration.TaskOrchestrationFactory;
import org.junit.jupiter.api.AfterEach;
import java.time.Duration;
@@ -67,6 +68,16 @@ public String getName() {
public TaskOrchestration create() {
return implementation;
}
+
+ @Override
+ public String getVersionName() {
+ return "";
+ }
+
+ @Override
+ public Boolean isLatestVersion() {
+ return false;
+ }
});
return this;
}
diff --git a/examples/src/main/java/io/dapr/examples/workflows/utils/PropertyUtils.java b/examples/src/main/java/io/dapr/examples/workflows/utils/PropertyUtils.java
index 9d64e45d36..7ea94e2f45 100644
--- a/examples/src/main/java/io/dapr/examples/workflows/utils/PropertyUtils.java
+++ b/examples/src/main/java/io/dapr/examples/workflows/utils/PropertyUtils.java
@@ -25,8 +25,11 @@ public static Properties getProperties(String[] args) {
properties = new Properties(new HashMap<>() {{
put(Properties.GRPC_PORT, args[0]);
}});
+
}
return properties;
}
+
+
}
diff --git a/examples/src/main/java/io/dapr/examples/workflows/versioning/README.md b/examples/src/main/java/io/dapr/examples/workflows/versioning/README.md
new file mode 100644
index 0000000000..c4137fcbd8
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/versioning/README.md
@@ -0,0 +1,4 @@
+dapr run --app-id workerV1 --resources-path ./components/workflows --dapr-grpc-port 50002 -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.versioning.full.VersioningWorkerV1 50002
+
+java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.workflows.versioning.full.VersioningClient 50002
+dapr stop --app-id workerV1
\ No newline at end of file
diff --git a/examples/src/main/java/io/dapr/examples/workflows/versioning/full/SendEventClient.java b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/SendEventClient.java
new file mode 100644
index 0000000000..3027646ee2
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/SendEventClient.java
@@ -0,0 +1,39 @@
+package io.dapr.examples.workflows.versioning.full;
+
+import io.dapr.examples.workflows.utils.PropertyUtils;
+import io.dapr.examples.workflows.utils.RetryUtils;
+import io.dapr.workflows.client.DaprWorkflowClient;
+import io.dapr.workflows.client.WorkflowState;
+
+import java.time.Duration;
+import java.util.concurrent.TimeoutException;
+
+public class SendEventClient {
+ /**
+ * The main method to start the client.
+ *
+ * @param args Input arguments (unused).
+ * @throws InterruptedException If program has been interrupted.
+ */
+ public static void main(String[] args) throws InterruptedException {
+ try (DaprWorkflowClient client = new DaprWorkflowClient(PropertyUtils.getProperties(args))) {
+ // Schedule an orchestration which will reliably count the number of words in all the given sentences.
+ String instanceId = RetryUtils.callWithRetry(() -> client.scheduleNewWorkflow(
+ VersioningWorkerV1.FullVersionWorkflowV1.class), Duration.ofSeconds(60));
+
+ System.out.printf("Started a new V1 workflow with instance ID: %s%n", instanceId);
+
+ // Block until the orchestration completes. Then print the final status, which includes the output.
+ WorkflowState workflowState = client.waitForWorkflowCompletion(
+ instanceId,
+ Duration.ofSeconds(30),
+ true);
+ System.out.printf("workflow instance with ID: %s completed with result: %s%n", instanceId,
+ workflowState.readOutputAs(String.class));
+ } catch (TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
+
+
diff --git a/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningClient.java b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningClient.java
new file mode 100644
index 0000000000..fd0d107f46
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningClient.java
@@ -0,0 +1,38 @@
+package io.dapr.examples.workflows.versioning.full;
+
+import io.dapr.examples.workflows.utils.PropertyUtils;
+import io.dapr.examples.workflows.utils.RetryUtils;
+import io.dapr.workflows.client.DaprWorkflowClient;
+import io.dapr.workflows.client.WorkflowState;
+
+import java.time.Duration;
+import java.util.concurrent.TimeoutException;
+
+public class VersioningClient {
+ /**
+ * The main method to start the client.
+ *
+ * @param args Input arguments (unused).
+ * @throws InterruptedException If program has been interrupted.
+ */
+ public static void main(String[] args) throws InterruptedException {
+ try (DaprWorkflowClient client = new DaprWorkflowClient(PropertyUtils.getProperties(args))) {
+ // Schedule an orchestration which will reliably count the number of words in all the given sentences.
+ String instanceId = RetryUtils.callWithRetry(() -> client.scheduleNewWorkflow("VersioningWorker"), Duration.ofSeconds(60));
+
+ System.out.printf("Started a new V1 workflow with instance ID: %s%n", instanceId);
+
+ // Block until the orchestration completes. Then print the final status, which includes the output.
+ WorkflowState workflowState = client.waitForWorkflowCompletion(
+ instanceId,
+ Duration.ofSeconds(30),
+ true);
+ System.out.printf("workflow instance with ID: %s completed with result: %s%n", instanceId,
+ workflowState.readOutputAs(String.class));
+ } catch (TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
+
+
diff --git a/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningWorkerV1.java b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningWorkerV1.java
new file mode 100644
index 0000000000..ede78f5425
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningWorkerV1.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 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.versioning.full;
+
+import io.dapr.examples.workflows.utils.PropertyUtils;
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import io.dapr.workflows.runtime.WorkflowRuntime;
+import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
+
+public class VersioningWorkerV1 {
+
+
+ public static final String ACTIVITY_1 = "Activity1";
+ public static final String ACTIVITY_2 = "Activity2";
+
+ /**
+ * The main method of this app.
+ *
+ * @param args The port the app will listen on.
+ * @throws Exception An Exception.
+ */
+ public static void main(String[] args) throws Exception {
+ // Register the Workflow with the builder.
+ WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(PropertyUtils.getProperties(args)).registerWorkflow("VersioningWorker", VersioningWorkerV1.FullVersionWorkflowV1.class, "V1", true);
+ builder.registerActivity(ACTIVITY_1, (ctx -> {
+ System.out.println("Activity1 called.");
+ return ACTIVITY_1;
+ }));
+ builder.registerActivity(ACTIVITY_2, (ctx -> {
+ System.out.println("Activity2 called.");
+ return ACTIVITY_2;
+ }));
+
+ // Build and then start the workflow runtime pulling and executing tasks
+ WorkflowRuntime runtime = builder.build();
+ runtime.start();
+ }
+
+ public static class FullVersionWorkflowV1 implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow V1: {}", ctx.getName());
+
+ String result = "";
+ result += ctx.callActivity(VersioningWorkerV1.ACTIVITY_1, String.class).await() +", ";
+ ctx.waitForExternalEvent("test").await();
+ result += ctx.callActivity(VersioningWorkerV1.ACTIVITY_2, String.class).await();
+
+ ctx.getLogger().info("Workflow finished with result: {}", result);
+ ctx.complete(result);
+ };
+ }
+ }
+
+}
diff --git a/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningWorkerV2.java b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningWorkerV2.java
new file mode 100644
index 0000000000..e2849c07e7
--- /dev/null
+++ b/examples/src/main/java/io/dapr/examples/workflows/versioning/full/VersioningWorkerV2.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 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.versioning.full;
+
+import io.dapr.examples.workflows.utils.PropertyUtils;
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import io.dapr.workflows.runtime.WorkflowRuntime;
+import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
+
+public class VersioningWorkerV2 {
+
+ public static final String ACTIVITY_3 = "Activity3";
+ public static final String ACTIVITY_4 = "Activity4";
+
+ /**
+ * The main method of this app.
+ *
+ * @param args The port the app will listen on.
+ * @throws Exception An Exception.
+ */
+ public static void main(String[] args) throws Exception {
+ // Register the Workflow with the builder.
+ WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder(PropertyUtils.getProperties(args))
+ .registerWorkflow("VersioningWorker", VersioningWorkerV1.FullVersionWorkflowV1.class, "V1", true)
+ .registerWorkflow("VersioningWorker", FullVersionWorkflowV2.class, "V2", true);
+ builder.registerActivity(ACTIVITY_3, (ctx -> {
+ System.out.println("Activity3 called.");
+ return ACTIVITY_3;
+ }));
+ builder.registerActivity(ACTIVITY_4, (ctx -> {
+ System.out.println("Activity4 called.");
+ return ACTIVITY_4;
+ }));
+
+ // Build and then start the workflow runtime pulling and executing tasks
+ WorkflowRuntime runtime = builder.build();
+ runtime.start();
+ }
+
+
+ public static class FullVersionWorkflowV2 implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow V2: {}", ctx.getName());
+
+ String result = "";
+ result += ctx.callActivity(VersioningWorkerV2.ACTIVITY_3, String.class).await() +", ";
+ result += ctx.callActivity(VersioningWorkerV2.ACTIVITY_4, String.class).await();
+
+ ctx.getLogger().info("Workflow finished with result: {}", result);
+ ctx.complete(result);
+ };
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 37c6ecea7d..63596d0ed9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,7 +53,7 @@
2.0
1.21.3
- 3.4.9
+ 3.4.13
6.2.7
1.7.0
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/FullVersioningWorkflowsIT.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/FullVersioningWorkflowsIT.java
new file mode 100644
index 0000000000..81f4d58eba
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/FullVersioningWorkflowsIT.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2024 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.version.full;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.dapr.config.Properties;
+import io.dapr.it.spring.data.CustomMySQLContainer;
+import io.dapr.it.testcontainers.ContainerConstants;
+import io.dapr.it.testcontainers.workflows.TestWorkflowsApplication;
+import io.dapr.it.testcontainers.workflows.TestWorkflowsConfiguration;
+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.WorkflowState;
+import io.dapr.workflows.runtime.WorkflowRuntime;
+import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.test.context.DynamicPropertyRegistry;
+import org.springframework.test.context.DynamicPropertySource;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.MySQLContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.containers.wait.strategy.WaitStrategy;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.HashMap;
+import java.util.Map;
+
+import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME;
+import static io.dapr.testcontainers.DaprContainerConstants.DAPR_PLACEMENT_IMAGE_TAG;
+import static io.dapr.testcontainers.DaprContainerConstants.DAPR_SCHEDULER_IMAGE_TAG;
+import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@SpringBootTest(
+ webEnvironment = WebEnvironment.RANDOM_PORT,
+ classes = {
+ TestWorkflowsConfiguration.class,
+ TestWorkflowsApplication.class
+ }
+)
+@Testcontainers
+@Tag("testcontainers")
+public class FullVersioningWorkflowsIT {
+
+ private static final Network DAPR_NETWORK = Network.newNetwork();
+
+ private static final WaitStrategy MYSQL_WAIT_STRATEGY = Wait
+ .forLogMessage(".*port: 3306 MySQL Community Server \\(GPL\\).*", 1)
+ .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS));
+
+ private static final String STATE_STORE_DSN = "mysql:password@tcp(mysql:3306)/";
+ private static final Map STATE_STORE_PROPERTIES = createStateStoreProperties();
+
+ @Container
+ private static final MySQLContainer> MY_SQL_CONTAINER = new CustomMySQLContainer<>("mysql:5.7.34")
+ .withNetworkAliases("mysql")
+ .withDatabaseName("dapr_db")
+ .withUsername("mysql")
+ .withPassword("password")
+ .withNetwork(DAPR_NETWORK)
+ .waitingFor(MYSQL_WAIT_STRATEGY);
+
+ @Container
+ private final static DaprPlacementContainer sharedPlacementContainer = new DaprPlacementContainer(DAPR_PLACEMENT_IMAGE_TAG)
+ .withNetwork(DAPR_NETWORK)
+ .withNetworkAliases("placement")
+ .withReuse(false);
+
+ @Container
+ private final static DaprSchedulerContainer sharedSchedulerContainer = new DaprSchedulerContainer(DAPR_SCHEDULER_IMAGE_TAG)
+ .withNetwork(DAPR_NETWORK)
+ .withNetworkAliases("scheduler")
+ .withReuse(false);
+
+ @Container
+ private static final DaprContainer DAPR_CONTAINER_V1 = new DaprContainer(DockerImageName.parse("daprio/daprd:1.17.0-rc.2").asCompatibleSubstituteFor("daprio/daprd:1.16.0-rc.5"))
+ .withAppName("dapr-worker-v1")
+ .withNetwork(DAPR_NETWORK)
+ .withComponent(new Component(STATE_STORE_NAME, "state.mysql", "v1", STATE_STORE_PROPERTIES))
+ .withPlacementContainer(sharedPlacementContainer)
+ .withSchedulerContainer(sharedSchedulerContainer)
+ .withDaprLogLevel(DaprLogLevel.DEBUG)
+ .withLogConsumer(outputFrame -> System.out.println("daprV1 -> " +outputFrame.getUtf8String()))
+ .withAppChannelAddress("host.testcontainers.internal")
+ .dependsOn(MY_SQL_CONTAINER, sharedPlacementContainer, sharedSchedulerContainer);
+
+ @Container
+ private static final DaprContainer DAPR_CONTAINER_V2 = new DaprContainer(DockerImageName.parse("daprio/daprd:1.17.0-rc.2").asCompatibleSubstituteFor("daprio/daprd:1.16.0-rc.5"))
+ .withAppName("dapr-worker-v2")
+ .withNetwork(DAPR_NETWORK)
+ .withComponent(new Component(STATE_STORE_NAME, "state.mysql", "v1", STATE_STORE_PROPERTIES))
+ .withPlacementContainer(sharedPlacementContainer)
+ .withSchedulerContainer(sharedSchedulerContainer)
+ .withDaprLogLevel(DaprLogLevel.DEBUG)
+ .withLogConsumer(outputFrame -> System.out.println("daprV2 -> " + outputFrame.getUtf8String()))
+ .withAppChannelAddress("host.testcontainers.internal")
+ .dependsOn(MY_SQL_CONTAINER, sharedPlacementContainer, sharedSchedulerContainer);
+
+ @Container
+ private final static GenericContainer> workerV1 = new GenericContainer<>(ContainerConstants.JDK_17_TEMURIN_JAMMY)
+ .withCopyFileToContainer(MountableFile.forHostPath("target"), "/app")
+ .withWorkingDirectory("/app")
+ .withCommand("java", "-cp", "test-classes:classes:dependency/*:*",
+ "-Ddapr.app.id=dapr-worker-v1",
+ "-Ddapr.grpc.endpoint=dapr-worker-v1:50001",
+ "-Ddapr.http.endpoint=dapr-worker-v1:3500",
+ "io.dapr.it.testcontainers.workflows.version.full.WorkflowV2Worker")
+ .withNetwork(DAPR_NETWORK)
+ .dependsOn(DAPR_CONTAINER_V1)
+ .waitingFor(Wait.forLogMessage(".*WorkerV1 started.*", 1))
+ .withLogConsumer(outputFrame -> System.out.println("WorkerV1: " + outputFrame.getUtf8String()));
+
+// This container will be started manually
+ private final static GenericContainer> workerV2 = new GenericContainer<>(ContainerConstants.JDK_17_TEMURIN_JAMMY)
+ .withCopyFileToContainer(MountableFile.forHostPath("target"), "/app")
+ .withWorkingDirectory("/app")
+ .withCommand("java", "-cp", "test-classes:classes:dependency/*:*",
+ "-Ddapr.app.id=dapr-worker-v2",
+ "-Ddapr.grpc.endpoint=dapr-worker-v2:50001",
+ "-Ddapr.http.endpoint=dapr-worker-v2:3500",
+ "io.dapr.it.testcontainers.workflows.version.full.WorkflowV1Worker")
+ .withNetwork(DAPR_NETWORK)
+ .dependsOn(DAPR_CONTAINER_V2)
+ .waitingFor(Wait.forLogMessage(".*WorkerV2 started.*", 1))
+ .withLogConsumer(outputFrame -> System.out.println("WorkerV2: " + outputFrame.getUtf8String()));
+
+
+ private static Map createStateStoreProperties() {
+ Map result = new HashMap<>();
+
+ result.put("keyPrefix", "name");
+ result.put("schemaName", "dapr_db");
+ result.put("actorStateStore", "true");
+ result.put("connectionString", STATE_STORE_DSN);
+
+ return result;
+ }
+
+ @DynamicPropertySource
+ static void daprProperties(DynamicPropertyRegistry registry) {
+ registry.add("dapr.http.endpoint", DAPR_CONTAINER_V1::getHttpEndpoint);
+ registry.add("dapr.grpc.endpoint", DAPR_CONTAINER_V1::getGrpcEndpoint);
+ }
+
+ @Test
+ public void testWorkflows() throws Exception {
+ DaprWorkflowClient workflowClientV1 = daprWorkflowClient(DAPR_CONTAINER_V1.getHttpEndpoint(), DAPR_CONTAINER_V1.getGrpcEndpoint());
+// Start workflow V1
+ String instanceIdV1 = workflowClientV1.scheduleNewWorkflow("VersionWorkflow");
+ workflowClientV1.waitForWorkflowStart(instanceIdV1, Duration.ofSeconds(10), false);
+
+
+ workerV2.start();
+ DaprWorkflowClient workflowClientV2 = daprWorkflowClient(DAPR_CONTAINER_V2.getHttpEndpoint(), DAPR_CONTAINER_V2.getGrpcEndpoint());
+
+ // Start workflow V2
+ String instanceIdV2 = workflowClientV2.scheduleNewWorkflow("VersionWorkflow");
+ workflowClientV2.waitForWorkflowStart(instanceIdV2, Duration.ofSeconds(10), false);
+
+// Continue workflow V1
+ workflowClientV1.raiseEvent(instanceIdV1, "test", null);
+
+ // Wait for workflow to complete
+ Duration timeout = Duration.ofSeconds(10);
+ WorkflowState workflowStatusV1 = workflowClientV1.waitForWorkflowCompletion(instanceIdV1, timeout, true);
+ WorkflowState workflowStatusV2 = workflowClientV2.waitForWorkflowCompletion(instanceIdV2, timeout, true);
+
+ assertNotNull(workflowStatusV1);
+ assertNotNull(workflowStatusV2);
+
+ String resultV1 = workflowStatusV1.readOutputAs(String.class);
+ assertEquals("Activity1, Activity2", resultV1);
+
+ String resultV2 = workflowStatusV1.readOutputAs(String.class);
+ assertEquals("Activity3, Activity4", resultV2);
+ }
+
+
+
+ public DaprWorkflowClient daprWorkflowClient(
+ String daprHttpEndpoint,
+ String daprGrpcEndpoint
+ ){
+ Map overrides = Map.of(
+ "dapr.http.endpoint", daprHttpEndpoint,
+ "dapr.grpc.endpoint", daprGrpcEndpoint
+ );
+
+ return new DaprWorkflowClient(new Properties(overrides));
+ }
+
+
+}
+
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/VersionedWorkflows.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/VersionedWorkflows.java
new file mode 100644
index 0000000000..0d835c9be8
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/VersionedWorkflows.java
@@ -0,0 +1,81 @@
+package io.dapr.it.testcontainers.workflows.version.full;
+
+import io.dapr.workflows.Workflow;
+import io.dapr.workflows.WorkflowStub;
+import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
+
+public class VersionedWorkflows {
+ public static final String ACTIVITY_1 = "Activity1";
+ public static final String ACTIVITY_2 = "Activity2";
+ public static final String ACTIVITY_3 = "Activity3";
+ public static final String ACTIVITY_4 = "Activity4";
+
+
+
+ public static class FullVersionWorkflowV1 implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow V1: {}", ctx.getName());
+
+ String result = "";
+ result += ctx.callActivity(VersionedWorkflows.ACTIVITY_1, String.class).await() +", ";
+ ctx.waitForExternalEvent("test").await();
+ result += ctx.callActivity(VersionedWorkflows.ACTIVITY_2, String.class).await();
+
+ ctx.getLogger().info("Workflow finished with result: {}", result);
+ ctx.complete(result);
+ };
+ }
+ }
+
+ public static class FullVersionWorkflowV2 implements Workflow {
+ @Override
+ public WorkflowStub create() {
+ return ctx -> {
+ ctx.getLogger().info("Starting Workflow V2: {}", ctx.getName());
+
+ String result = "";
+ result += ctx.callActivity(VersionedWorkflows.ACTIVITY_3, String.class).await() +", ";
+ result += ctx.callActivity(VersionedWorkflows.ACTIVITY_4, String.class).await();
+
+ ctx.getLogger().info("Workflow finished with result: {}", result);
+ ctx.complete(result);
+ };
+ }
+ }
+
+ public static void addWorkflowV1(WorkflowRuntimeBuilder workflowRuntimeBuilder) {
+ workflowRuntimeBuilder.registerWorkflow("VersionWorkflow",
+ VersionedWorkflows.FullVersionWorkflowV1.class,
+ "V1",
+ true);
+
+ workflowRuntimeBuilder.registerActivity(VersionedWorkflows.ACTIVITY_1, (ctx -> {
+ System.out.println("Activity1 called.");
+ return VersionedWorkflows.ACTIVITY_1;
+ }));
+
+ workflowRuntimeBuilder.registerActivity(VersionedWorkflows.ACTIVITY_2, (ctx -> {
+ System.out.println("Activity2 called.");
+ return VersionedWorkflows.ACTIVITY_2;
+ }));
+ }
+
+ public static void addWorkflowV2(WorkflowRuntimeBuilder workflowRuntimeBuilder) {
+ workflowRuntimeBuilder.registerWorkflow("VersionWorkflow",
+ VersionedWorkflows.FullVersionWorkflowV2.class,
+ "V2",
+ true);
+
+ workflowRuntimeBuilder.registerActivity(VersionedWorkflows.ACTIVITY_3, (ctx -> {
+ System.out.println("Activity3 called.");
+ return VersionedWorkflows.ACTIVITY_3;
+ }));
+
+ workflowRuntimeBuilder.registerActivity(VersionedWorkflows.ACTIVITY_4, (ctx -> {
+ System.out.println("Activity4 called.");
+ return VersionedWorkflows.ACTIVITY_4;
+ }));
+ }
+}
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/WorkflowV1Worker.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/WorkflowV1Worker.java
new file mode 100644
index 0000000000..cdca0941b1
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/WorkflowV1Worker.java
@@ -0,0 +1,22 @@
+package io.dapr.it.testcontainers.workflows.version.full;
+
+import io.dapr.it.testcontainers.workflows.multiapp.MultiAppWorkflow;
+import io.dapr.workflows.runtime.WorkflowRuntime;
+import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
+
+public class WorkflowV1Worker {
+ public static void main(String[] args) throws Exception {
+ System.out.println("=== Starting Workflow V1 Runtime ===");
+
+ // Register the Workflow with the builder
+ WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder();
+ VersionedWorkflows.addWorkflowV1(builder);
+
+ // Build and start the workflow runtime
+ try (WorkflowRuntime runtime = builder.build()) {
+ System.out.println("WorkerV1 started");
+ System.out.println("Waiting for workflow orchestration requests...");
+ runtime.start();
+ }
+ }
+}
diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/WorkflowV2Worker.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/WorkflowV2Worker.java
new file mode 100644
index 0000000000..725adcfbab
--- /dev/null
+++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/workflows/version/full/WorkflowV2Worker.java
@@ -0,0 +1,22 @@
+package io.dapr.it.testcontainers.workflows.version.full;
+
+import io.dapr.workflows.runtime.WorkflowRuntime;
+import io.dapr.workflows.runtime.WorkflowRuntimeBuilder;
+
+public class WorkflowV2Worker {
+ public static void main(String[] args) throws Exception {
+ System.out.println("=== Starting Workflow V2 Runtime ===");
+
+ // Register the Workflow with the builder
+ WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder();
+ VersionedWorkflows.addWorkflowV1(builder);
+ VersionedWorkflows.addWorkflowV2(builder);
+
+ // Build and start the workflow runtime
+ try (WorkflowRuntime runtime = builder.build()) {
+ System.out.println("WorkerV2 started");
+ System.out.println("Waiting for workflow orchestration requests...");
+ runtime.start();
+ }
+ }
+}
diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml
index 7fd95807f1..6346811f25 100644
--- a/sdk-workflows/pom.xml
+++ b/sdk-workflows/pom.xml
@@ -22,6 +22,10 @@
dapr-sdk
${project.parent.version}