* Edits to python versioning fixes * add missing workflow imports
9.8 KiB
Python SDK Versioning
For conceptual overview and guidance on choosing an approach, see references/core/versioning.md.
Patching API
The patched() Function
The patched() function checks whether a Workflow should run new or old code:
from temporalio import workflow
@workflow.defn
class ShippingWorkflow:
@workflow.run
async def run(self) -> None:
if workflow.patched("send-email-instead-of-fax"):
# New code path
await workflow.execute_activity(
send_email,
start_to_close_timeout=timedelta(minutes=5),
)
else:
# Old code path (for replay of existing workflows)
await workflow.execute_activity(
send_fax,
start_to_close_timeout=timedelta(minutes=5),
)
How it works:
- For new executions:
patched()returnsTrueand records a marker in the Workflow history - For replay with the marker:
patched()returnsTrue(history includes this patch) - For replay without the marker:
patched()returnsFalse(history predates this patch)
Python-specific behavior: The patched() return value is memoized on first call. This means you cannot reliably use patched() in loops—it will return the same value every iteration. Workaround: append a sequence number to the patch ID for each iteration (e.g., f"my-change-{i}").
Three-Step Patching Process
Patching is a three-step process for safely deploying changes.
Warning: Failing to follow this process correctly will result in non-determinism errors for in-flight workflows.
Step 1: Patch in New Code
Add the patch with both old and new code paths:
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order: Order) -> str:
if workflow.patched("add-fraud-check"):
# New: Run fraud check before payment
await workflow.execute_activity(
check_fraud,
order,
start_to_close_timeout=timedelta(minutes=2),
)
# Original payment logic runs for both paths
return await workflow.execute_activity(
process_payment,
order,
start_to_close_timeout=timedelta(minutes=5),
)
Step 2: Deprecate the Patch
Once all pre-patch Workflow Executions have completed, remove the old code and use deprecate_patch():
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order: Order) -> str:
workflow.deprecate_patch("add-fraud-check")
# Only new code remains
await workflow.execute_activity(
check_fraud,
order,
start_to_close_timeout=timedelta(minutes=2),
)
return await workflow.execute_activity(
process_payment,
order,
start_to_close_timeout=timedelta(minutes=5),
)
Step 3: Remove the Patch
After all workflows with the deprecated patch marker have completed, remove the deprecate_patch() call entirely:
@workflow.defn
class OrderWorkflow:
@workflow.run
async def run(self, order: Order) -> str:
await workflow.execute_activity(
check_fraud,
order,
start_to_close_timeout=timedelta(minutes=2),
)
return await workflow.execute_activity(
process_payment,
order,
start_to_close_timeout=timedelta(minutes=5),
)
Query Filters for Finding Workflows by Version
Use List Filters to find workflows with specific patch versions:
# Find running workflows with a specific patch
temporal workflow list --query \
'WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running" AND TemporalChangeVersion = "add-fraud-check"'
# Find running workflows without any patch (pre-patch versions)
temporal workflow list --query \
'WorkflowType = "OrderWorkflow" AND ExecutionStatus = "Running" AND TemporalChangeVersion IS NULL'
Workflow Type Versioning
For incompatible changes, create a new Workflow Type instead of using patches:
@workflow.defn(name="PizzaWorkflow")
class PizzaWorkflow:
@workflow.run
async def run(self, order: PizzaOrder) -> str:
# Original implementation
return await self._process_order_v1(order)
@workflow.defn(name="PizzaWorkflowV2")
class PizzaWorkflowV2:
@workflow.run
async def run(self, order: PizzaOrder) -> str:
# New implementation with incompatible changes
return await self._process_order_v2(order)
Register both with the Worker:
worker = Worker(
client,
task_queue="pizza-task-queue",
workflows=[PizzaWorkflow, PizzaWorkflowV2],
activities=[make_pizza, deliver_pizza],
)
Update client code to start new workflows with the new type:
# Old workflows continue on PizzaWorkflow
# New workflows use PizzaWorkflowV2
handle = await client.start_workflow(
PizzaWorkflowV2.run,
order,
id=f"pizza-{order.id}",
task_queue="pizza-task-queue",
)
Check for open executions before removing the old type:
temporal workflow list --query 'WorkflowType = "PizzaWorkflow" AND ExecutionStatus = "Running"'
Worker Versioning
Worker Versioning manages versions at the deployment level, allowing multiple Worker versions to run simultaneously.
Key Concepts
Worker Deployment: A logical service grouping similar Workers together (e.g., "loan-processor"). All versions of your code live under this umbrella.
Worker Deployment Version: A specific snapshot of your code identified by a deployment name and Build ID (e.g., "loan-processor:v1.0" or "loan-processor:abc123").
Configuring Workers for Versioning
from temporalio.worker import Worker
from temporalio.worker.deployment_config import (
WorkerDeploymentConfig,
WorkerDeploymentVersion,
)
worker = Worker(
client,
task_queue="my-task-queue",
workflows=[MyWorkflow],
activities=[my_activity],
deployment_config=WorkerDeploymentConfig(
version=WorkerDeploymentVersion(
deployment_name="my-service",
build_id="v1.0.0", # or git commit hash
),
use_worker_versioning=True,
),
)
Configuration parameters:
use_worker_versioning: Enables Worker Versioningversion: Identifies the Worker Deployment Version (deployment name + build ID)- Build ID: Typically a git commit hash, version number, or timestamp
PINNED vs AUTO_UPGRADE Behaviors
PINNED Behavior
Workflows stay locked to their original Worker version:
from temporalio import workflow
from temporalio.common import VersioningBehavior
@workflow.defn(versioning_behavior=VersioningBehavior.PINNED)
class StableWorkflow:
@workflow.run
async def run(self) -> str:
return await workflow.execute_activity(
process_order,
start_to_close_timeout=timedelta(minutes=5),
)
When to use PINNED:
- Short-running workflows (minutes to hours)
- Consistency is critical (e.g., financial transactions)
- You want to eliminate version compatibility complexity
- Building new applications and want simplest development experience
AUTO_UPGRADE Behavior
Workflows can move to newer versions:
from temporalio import workflow
from temporalio.common import VersioningBehavior
@workflow.defn(versioning_behavior=VersioningBehavior.AUTO_UPGRADE)
class UpgradableWorkflow:
@workflow.run
async def run(self) -> str:
return await workflow.execute_activity(
process_order,
start_to_close_timeout=timedelta(minutes=5),
)
When to use AUTO_UPGRADE:
- Long-running workflows (weeks or months)
- Workflows need to benefit from bug fixes during execution
- Migrating from traditional rolling deployments
- You are already using patching APIs for version transitions
Important: AUTO_UPGRADE workflows still need patching to handle version transitions safely since they can move between Worker versions.
Worker Configuration with Default Behavior
worker = Worker(
client,
task_queue="orders-task-queue",
workflows=[OrderWorkflow],
activities=[process_order],
deployment_config=WorkerDeploymentConfig(
version=WorkerDeploymentVersion(
deployment_name="order-service",
build_id=os.environ["BUILD_ID"],
),
use_worker_versioning=True,
default_versioning_behavior=VersioningBehavior.PINNED,
),
)
Deployment Strategies
Blue-Green Deployments
Maintain two environments and switch traffic between them:
- Deploy new code to idle environment
- Run tests and validation
- Switch traffic to new environment
- Keep old environment for instant rollback
Rainbow Deployments
Multiple versions run simultaneously:
- New workflows use latest version
- Existing workflows complete on their original version
- Add new versions alongside existing ones
- Gradually sunset old versions as workflows complete
This works well with Kubernetes where you manage multiple ReplicaSets running different Worker versions.
Querying Workflows by Worker Version
# Find workflows on a specific Worker version
temporal workflow list --query \
'TemporalWorkerDeploymentVersion = "my-service:v1.0.0" AND ExecutionStatus = "Running"'
Best Practices
- Check for open executions before removing old code paths
- Use descriptive patch IDs that explain the change (e.g., "add-fraud-check" not "patch-1")
- Deploy patches incrementally: patch, deprecate, remove
- Use PINNED for short workflows to simplify version management
- Use AUTO_UPGRADE with patching for long-running workflows that need updates
- Generate Build IDs from code (git hash) to ensure changes produce new versions
- Avoid rolling deployments for high-availability services with long-running workflows