Skip to main content

Versioning - PHP SDK feature guide

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 MyWorkflowV2V2:

#[WorkflowInterface]
class MyWorkflow
{}

#[WorkflowInterface]
class MyWorkflowV2
{}

You would then need to update the Worker configuration, and any other identifier strings, to register both Workflow Types. 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.

Patching with GetVersion

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:

#[WorkflowInterface]
class MyWorkflow
{
private $activity;

public function __construct()
{
$this->activity = Workflow::newActivityStub(
YourActivityInterface::class,
ActivityOptions::new()->withScheduleToStartTimeout(60)
);
}

#[WorkflowMethod]
public function runAsync()
{
$result = yield $this->activity->prePatchActivity();
}
}

Suppose you replaced prePatchActivity with postPatchActivity and deployed the updated code.

If an existing Workflow Execution was started by the original version of the Workflow code, where prePatchActivity was run, and then resumed running on a new Worker where it was replaced with postPatchActivity, the server side Event History would be out of sync. This would cause the Workflow to fail with a nondeterminism error.

To resolve this, you can use Workflow::getVersion to patch to your Workflow:

#[WorkflowInterface]
class MyWorkflow
{
// ...

#[WorkflowMethod]
public function runAsync()
{
$version = yield Workflow::getVersion('Step 1', Workflow::DEFAULT_VERSION, 1);

$result = $version === Workflow::DEFAULT_VERSION
? yield $this->activity->prePatchActivity()
: yield $this->activity->postPatchActivity();
}
}

When getVersion() is run for the new Workflow Execution, it records a marker in the Event History so that all future calls to getVersion() for this change Id — Step 1 in the example — on this Workflow Execution will always return the given version number, which is 1 in the example.

If you make an additional change, such as adding anotherPatchActivity(), you need to add some additional code:

#[WorkflowInterface]
class MyWorkflow
{
// ...

#[WorkflowMethod]
public function runAsync()
{
$version = yield Workflow::getVersion('Step 1', Workflow::DEFAULT_VERSION, maxSupported: 2);

$result = match($version) {
Workflow::DEFAULT_VERSION => yield $this->activity->prePatchActivity()
1 => yield $this->activity->postPatchActivity();
2 => yield $this->activity->anotherPatchActivity();
};
}
}

Note that we changed maxSupported from 1 to 2. A Workflow that has already passed this getVersion() call before it was introduced returns DEFAULT_VERSION. A Workflow that was run with maxSupported set to 1 returns 1. New Workflows return 2.

After you are sure that all of the Workflow Executions prior to version 1 have completed, you can remove the code for that version:

    #[WorkflowMethod]
public function runAsync()
{
$version = yield Workflow::getVersion('Step 1', minSupported: 1, maxSupported: 2);

$result = match($version) {
1 => yield $this->activity->postPatchActivity();
2 => yield $this->activity->anotherPatchActivity();
};
}

You'll note that minSupported has changed from DEFAULT_VERSION to 1. If an older version of the Workflow Execution history is replayed on this code, it fails because the minimum expected version is 1. After you are sure that all of the Workflow Executions for version 1 have completed, you can remove version 1 so that your code looks like the following:

    #[WorkflowMethod]
public function runAsync()
{
$version = yield Workflow::getVersion('Step 1', minSupported: 2, maxSupported: 2);

$result = yield $this->activity->anotherPatchActivity();
}

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 Workflows to specific revisions, avoiding the need for patching.

Runtime checking

The Temporal PHP SDK performs a runtime check to help prevent obvious incompatible changes. Adding, removing, or reordering any of these methods without Versioning triggers the runtime check and results in a nondeterminism error:

  • workflow.ExecuteActivity()
  • workflow.ExecuteChildWorkflow()
  • workflow.NewTimer()
  • workflow.RequestCancelWorkflow()
  • workflow.SideEffect()
  • workflow.SignalExternalWorkflow()
  • workflow.Sleep()

The runtime check does not perform a thorough check. For example, it does not check on the Activity's input arguments or the Timer duration. Each Temporal SDK implements these sanity checks differently, and they are not a complete check for non-deterministic changes. Instead, you should incorporate Replay Testing when making revisions.