Versioning - .NET SDK
Since Workflow Executions in Temporal can run for long periods — sometimes months or even years — it's common to need to make changes to a Workflow Definition, even while a particular Workflow Execution is in progress.
The Temporal Platform requires that Workflow code is deterministic. If you make a change to your Workflow code that would cause non-deterministic behavior on Replay, you'll need to use one of our Versioning methods to gracefully update your running Workflows. With Versioning, you can modify your Workflow Definition so that new executions use the updated code, while existing ones continue running the original version. There are three primary Versioning methods that you can use:
- Workflow Type Versioning. This is the simplest of the three, and acts more like a cutover than true versioning. It is suitable for short-running Workflows.
- Versioning with Patching. This method works by adding branches to your code tied to specific revisions. It can be used to revise in-progress Workflows.
- Worker Versioning. The Worker Versioning feature allows you to tag your Workers and programmatically roll them out in deployment versions, so that old Workers can run old code paths and new Workers can run new code paths.
Workflow Type Versioning
Since incompatible changes only affect open Workflow Executions of the same type, you can avoid this problem by changing the Workflow Type for the new version. To do this, you can copy the Workflow Definition function, giving it a different name, and make sure that both names were registered with your Workers.
For example, if you had made an incompatible change to the following Workflow Definition:
[Workflow]
public class SayHelloWorkflow
{
[WorkflowRun]
# implementation code omitted for this example
}
then you would change the code as follows:
[Workflow]
public class SayHelloWorkflow
{
[WorkflowRun]
# this function contains the original code
}
[Workflow]
public class SayHelloWorkflowV2
{
[WorkflowRun]
# this function contains the updated code
}
You can use any name you like for the new function. Using some type of version identifier, such as V2 in this example, will make it easier to identify the change.
You would then update the Worker configuration to register both Workflow Types:
using var worker = new TemporalWorker(
client,
new TemporalWorkerOptions("greeting-tasks")
.AddWorkflow<SayHelloWorkflow>()
.AddWorkflow<SayHelloWorkflowV2>());
The downside of this method is that it does not use any Temporal platform features. It requires you to duplicate code and to update any code and commands used to start the Workflow. This can become impractical over time, depending on how you are providing configuration strings to your deployment. This method also does not provide a way to introduce versioning to any still-running Workflows -- it is essentially just a cutover, unlike the Patching method.
Versioning with Patching
Patching essentially defines a logical branch for a specific change in the Workflow. If your Workflow is not pinned to a specific deployment or you need to fix a bug in a running workflow, you can patch it.
Suppose you have an initial Workflow version called PrePatchActivity
:
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PrePatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
Now, you want to update your code to run PostPatchActivity
instead. This represents your desired end state.
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
The problem is that you cannot deploy PostPatchActivity
directly until you're certain there are no more running Workflows created using the PrePatchActivity
code, otherwise you are likely to cause a nondeterminism error.
Instead, you'll need to deploy PostPatchActivity
and use the Patched method to determine which version of the code to execute.
Patching is a three step process:
- Use Patched to patch in new code and run it alongside the old code.
- Remove the old code and apply DeprecatePatch.
- Once you're confident that all old Workflows have finished executing, remove
DeprecatePatch
.
Patching in new code
Using Patched
inserts a marker into the Workflow History.
During replay, if a Worker encounters a history with that marker, it will fail the Workflow task when the Workflow code doesn't produce the same patch marker (in this case, my-patch
). This ensures you can safely deploy code from PostPatchActivity
as a "feature flag" alongside the original version (PrePatchActivity
).
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
if (Workflow.Patched("my-patch"))
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
else
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PrePatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
}
// ...
}
}
Deprecating patches
After ensuring that all Workflows started with PrePatchActivity
code have finished, you can deprecate the patch.
Deprecated patches serve as a bridge between PrePatchActivity
and PostPatchActivity
. They function similarly to regular patches by adding a marker to the Workflow History. However, this marker won't cause a replay failure when the Workflow code doesn't produce it.
If, during the deployment of PostPatchActivity
, there are still live Workers running PrePatchActivity
code and these Workers pick up Workflow histories generated by PostPatchActivity
, they will safely use the patched branch.
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
Workflow.DeprecatePatch("my-patch")
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
Removing a patch
You can safely deploy PostPatchActivity
once all Workflows labeled my-patch or earlier are finished, based on the previously mentioned assertion.
[Workflow]
public class MyWorkflow
{
[WorkflowRun]
public async Task RunAsync()
{
this.result = await Workflow.ExecuteActivityAsync(
(MyActivities a) => a.PostPatchActivity(),
new() { StartToCloseTimeout = TimeSpan.FromMinutes(5) });
// ...
}
}
Patching allows you to make changes to currently running Workflows. It is a powerful method for introducing compatible changes without introducing non-determinism errors.
Worker Versioning
Temporal's Worker Versioning feature allows you to tag your Workers and programmatically roll them out in deployment versions, so that old Workers can run old code paths and new Workers can run new code paths. This way, you can pin your deployments to specific revisions, often avoiding the need for patching.
Changing the order of any commands in your Workflow code that interact directly with the Temporal Service -- such as calling an Activity or creating a Sleep timer -- will cause a non-determinism error unless you've implemented a versioning solution. To test whether a new revision will require versioning, you should incorporate Replay Testing.