Versioning - Ruby 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 two primary Versioning methods that you can use:
- 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 versioned deployments, so that old Workers can run old code paths and new Workers can run new code paths.
Versioning with Patching
To understand why Patching is useful, it's helpful to first demonstrate cutting over an entire Workflow.
Workflow cutovers
Since incompatible changes only affect open Workflow Executions of the same type, you can avoid determinism errors by creating a whole new Workflow when making changes. To do this, you can copy the Workflow Definition function, giving it a different name, and make sure that both names are registered with your Workers.
For example, you would duplicate MyWorkflow
as MyWorkflowV2
:
class MyWorkflow < Temporalio::Workflow::Definition
def execute
# ...
end
end
class MyWorkflowV2 < Temporalio::Workflow::Definition
def execute
# ...
end
end
You would then need to update the Worker configuration, and any other identifier strings, to register both Workflow Types:
client = Temporalio::Client.connect('localhost:7233', 'default')
worker = Temporalio::Worker.new(
client:,
task_queue: 'my-task-queue',
workflows: [MyWorkflow, MyWorkflowV2]
)
The downside of this method is that it requires you to duplicate code and to update any commands used to start the Workflow. This can become impractical over time. This method also does not provide a way to version any still-running Workflows -- it is essentially just a cutover, unlike Patching, which we will now demonstrate.
Adding a patch
Patching essentially defines a logical branch for a specific change in the Workflow. If your Workflow is not pinned to a specific Worker Deployment Version or you need to fix a bug in a running workflow, you can patch it.
Suppose you have an initial Workflow that runs PrePatchActivity
:
class MyWorkflow < Temporalio::Workflow::Definition
def execute
result = Temporalio::Workflow.execute_activity(
PrePatchActivity,
start_to_close_timeout: 100
)
# ...
end
end
Now, you want to update your code to run PostPatchActivity
instead. This represents your desired end state.
class MyWorkflow < Temporalio::Workflow::Definition
def execute
result = Temporalio::Workflow.execute_activity(
PostPatchActivity,
start_to_close_timeout: 100
)
# ...
end
end
The problem is that you cannot deploy this new revision 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 use the patched
function to check which version of the code should be executed.
Patching is a three-step process:
- Patch in any new, updated code using the
patched()
function. Run the new patched code alongside old code. - Remove old code and use
deprecate_patch()
to mark a particular patch as deprecated. - Once there are no longer any open Worklow Executions of the previous version of the code, remove
deprecatePatch()
. Let's walk through this process in sequence.
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 new code paths alongside the original branch.
class MyWorkflow < Temporalio::Workflow::Definition
def execute
if Temporalio::Workflow.patched('my-patch')
result = Temporalio::Workflow.execute_activity(
PostPatchActivity,
start_to_close_timeout: 100
)
else
result = Temporalio::Workflow.execute_activity(
PrePatchActivity,
start_to_close_timeout: 100
)
end
# ...
end
end
Deprecating patches
After ensuring that all Workflows started with v1
code have finished, you can deprecate the patch.
Once you're confident that your Workflows are no longer running the pre-patch code paths, you can deploy your code with deprecate_patch()
.
These Workers will be running the most up-to-date version of the Workflow code, which no longer requires the patch.
The deprecate_patch()
function works similarly to the patched()
function by recording a marker in the Workflow history.
This marker does not fail replay when Workflow code does not emit it.
Deprecated patches serve as a bridge between the pre-patch code paths and the post-patch code paths, and are useful for avoiding errors resulting from patched code paths in your Workflow history.
class MyWorkflow < Temporalio::Workflow::Definition
def execute
Temporalio::Workflow.deprecate_patch('my-patch')
result = Temporalio::Workflow.execute_activity(
PostPatchActivity,
start_to_close_timeout: 100
)
# ...
end
end
Removing a patch
Once you're sure that you will no longer need to Query or Replay any of your pre-patch Workflows, you can then safely deploy Workers that no longer use either the patched()
or deprecate_patch()
calls:
Patching allows you to make changes to currently running Workflows. It is a powerful method for introducing compatible changes without introducing non-determinism errors.
Testing a Workflow for replay safety
To determine whether your Workflow your needs a patch, or that you've patched it successfully, you should incorporate Replay Testing.
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 Workflows to specific revisions, avoiding the need for patching.