Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EDU-3824: Add best practice advice to Typescript Versioning #3303

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/develop/dotnet/versioning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,90 @@ Implementing patching involves three steps:
2. Remove the old code and apply [DeprecatePatch](https://dotnet.temporal.io/api/Temporalio.Workflows.Workflow.html#Temporalio_Workflows_Workflow_DeprecatePatch_System_String_).
3. Once you're confident that all old Workflows have finished executing, remove `DeprecatePatch`.

#### Overview

The following sample shows how the `patched()` function behaves, providing explanations at each stage of the patching flow:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a line about defining the version conditions in descending order?

Copy link
Member

@kevinawoo kevinawoo Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like

The best practice is to write your version conditionals in descending order so that newly executed workflows will set their marker as the latest version.

The default value for the version is "" (empty string)


<!--SNIPSTART dotnet-patching-example-->

```csharp
if (patched('v3')) {
// This is the newest version of the code.

// The above patched statement following will do
// one of the following three things:

// 1. If the execution is not Replaying, it will evaluate
// to true and write a Marker Event to the history
// with a patch id v3. This code block will run.
// 2. If the execution is Replaying, and the original
// run put a Patch ID v3 at this location in the event
// history, it will evaluate to True, and this code block
// will run.
// 3. If the execution is Replaying, and the original
// run has a Patch ID other than v3 at this location in the event
// history, it will evaluate to False, and this code block won't
// run.
} else if (patched('v2')) {
// This is the second version of the code.

// The above patched statement following will do
// one of the following three things:

// 1. If the execution is not Replaying, the execution
// won't get here because the first patched statement
// will be True.
// 2. If the execution is Replaying, and the original
// run put a Patch ID v2 marker at this location in the event
// history, it will evaluate to True, and this code block
// will run.
// 3. If the execution is Replaying, and the original
// run has a Patch ID other than v2 at this location in the event
// history, or doesn't have a patch marker at this location in the event
// history, it will evaluate to False, and this code block won't
// run.
} else {
// This is the original version of the code.
//
// The above patched statement following will do
// one of the following three things:
//
// 1. If the execution is not Replaying, the execution
// won't get here because the first patched statement
// will be True.
// 2. If the execution is Replaying, and the original
// run had a patch marker v3 or v2 at this location in the event
// history, the execution
// won't get here because the first or second patched statement
// will be True (respectively).
// 3. If the execution is Replaying, and condition 2
// doesn't hold, then it will run this code.
}
```
<!--SNIPEND-->

To add more clarity, the following sample shows how `patched()` will behave in a different conditional block.
In this case, the code's conditional block doesn't have the newest code at the top.
Because `patched()` will always return `true` when not Replaying, this snippet will run the `v2` branch instead of `v3` in new executions.

<!--SNIPSTART dotnet-patching-anti-example-->

```csharp
if (patched('v2')) {
// This is bad because when doing an original execution (i.e. not replaying),
// all patched statements evaluate to True (and put a marker
// in the event history), which means that new executions
// will use v2, and miss v3 below
}
else if (patched('v3')) {}
else {}
```

The moral of the story is that when not Replaying, `patched()` will return true and write the patch ID to the Event History.
And when Replaying, it will only return true if the patch ID matches that in the Event History.

<!--SNIPEND-->

### Patching in new code {#using-patched-for-workflow-history-markers}

Using `Patched` inserts a marker into the Workflow History.
Expand Down Expand Up @@ -183,3 +267,11 @@ public class MyWorkflow
}
}
```

### Best Practice of Using Classes as Arguments and Returns

As a side note on the Patching API, its behavior is why Temporal recommends using a single object as arguments and returns from Signals, Queries, Updates, and Activities, rather than using multiple arguments/returns.
The Patching API's main use case is to support branching in an `if` block of a method body.
It is not designed to be used to set different methods or method signatures for different Workflow Versions.

Because of this, Temporal recommends that each Signal, Activity, etc, accepts a single object and returns a single object, so the method signature can stay constant, and you can do your versioning logic using `patched()` within the method body.
98 changes: 97 additions & 1 deletion docs/develop/python/versioning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,97 @@ Implementing patching involves three steps:
2. Remove the old code and apply [deprecate_patch](https://python.temporal.io/temporalio.workflow.html#deprecate_patch).
3. Once you're confident that all old Workflows have finished executing, remove `deprecate_patch`.

#### Overview

The following sample shows how the `patched()` function behaves, providing explanations at each stage of the patching flow:

<!--SNIPSTART python-patching-example-->

```python
if patched('v3'):
# This is the newest version of the code.

# The above patched statement following will do
# one of the following three things:

# 1. If the execution is not Replaying, it will evaluate
# to true and write a Marker Event to the history
# with a patch id v3. This code block will run.
# 2. If the execution is Replaying, and the original
# run put a Patch ID v3 at this location in the event
# history, it will evaluate to True, and this code block
# will run.
# 3. If the execution is Replaying, and the original
# run has a Patch ID other than v3 at this location in the event
# history, it will evaluate to False, and this code block won't
# run.
pass
elif patched('v2'):
# This is the second version of the code.

# The above patched statement following will do
# one of the following three things:

# 1. If the execution is not Replaying, the execution
# won't get here because the first patched statement
# will be True.
# 2. If the execution is Replaying, and the original
# run put a Patch ID v2 marker at this location in the event
# history, it will evaluate to True, and this code block
# will run.
# 3. If the execution is Replaying, and the original
# run has a Patch ID other than v2 at this location in the event
# history, or doesn't have a patch marker at this location in the event
# history, it will evaluate to False, and this code block won't
# run.
pass
else:
# This is the original version of the code.

# The above patched statement following will do
# one of the following three things:

# 1. If the execution is not Replaying, the execution
# won't get here because the first patched statement
# will be True.
# 2. If the execution is Replaying, and the original
# run had a patch marker v3 or v2 at this location in the event
# history, the execution
# won't get here because the first or second patched statement
# will be True (respectively).
# 3. If the execution is Replaying, and condition 2
# doesn't hold, then it will run this code.
pass
```
<!--SNIPEND-->

To add more clarity, the following sample shows how `patched()` will behave in a different conditional block.
In this case, the code's conditional block doesn't have the newest code at the top.
Because `patched()` will always return `True` when not Replaying, this snippet will run the `v2` branch instead of `v3` in new executions.

<!--SNIPSTART python-patching-anti-example-->

```python
if patched('v2'):
# This is bad because when doing an original execution (i.e. not replaying),
# all patched statements evaluate to True (and put a marker
# in the event history), which means that new executions
# will use v2, and miss v3 below
pass
elif patched('v3'):
pass
else:
pass
```

The moral of the story is that when not Replaying, `patched()` will return `True` and write the patch ID to the Event History.
And when Replaying, it will only return true if the patch ID matches that in the Event History.

<!--SNIPEND-->

### Patching in new code {#using-patched-for-workflow-history-markers}

Using `patched` inserts a marker into the Workflow History.
Using `patched()` inserts a marker into the Workflow History.

![image](https://user-images.githubusercontent.com/6764957/139673361-35d61b38-ab94-401e-ae7b-feaa52eae8c6.png)

Expand Down Expand Up @@ -199,6 +287,14 @@ class MyWorkflow:
)
```

### Best Practice of Using Python Dataclasses as Arguments and Returns

As a side note on the Patching API, its behavior is why Temporal recommends using single dataclasses as arguments and returns from Signals, Queries, Updates, and Activities, rather than using multiple arguments.
The Patching API's main use case is to support branching in an `if` block of a method body.
It is not designed to be used to set different methods or method signatures for different Workflow Versions.

Because of this, Temporal recommends that each Signal, Activity, etc, accepts a single dataclass and returns a single dataclass, so the method signature can stay constant, and you can do your versioning logic using `patched()` within the method body.

## How to use Worker Versioning in Python {#worker-versioning}

:::caution
Expand Down
92 changes: 92 additions & 0 deletions docs/develop/typescript/versioning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,90 @@ Patching is a three-step process:
2. Remove old code and `deprecatePatch`
3. When you are sure all old Workflows are done executing, remove `deprecatePatch`

#### Overview

The following sample shows how the `patched()` function behaves, providing explanations at each stage of the patching flow:

<!--SNIPSTART typescript-patching-example-->

```ts
if (patched('v3')) {
// This is the newest version of the code.

// The above patched statement following will do
// one of the following three things:

// 1. If the execution is not Replaying, it will evaluate
// to true and write a Marker Event to the history
// with a patch id v3. This code block will run.
// 2. If the execution is Replaying, and the original
// run put a Patch ID v3 at this location in the event
// history, it will evaluate to True, and this code block
// will run.
// 3. If the execution is Replaying, and the original
// run has a Patch ID other than v3 at this location in the event
// history, it will evaluate to False, and this code block won't
// run.
} else if (patched('v2')) {
// This is the second version of the code.

// The above patched statement following will do
// one of the following three things:

// 1. If the execution is not Replaying, the execution
// won't get here because the first patched statement
// will be True.
// 2. If the execution is Replaying, and the original
// run put a Patch ID v2 marker at this location in the event
// history, it will evaluate to True, and this code block
// will run.
// 3. If the execution is Replaying, and the original
// run has a Patch ID other than v2 at this location in the event
// history, or doesn't have a patch marker at this location in the event
// history, it will evaluate to False, and this code block won't
// run.
} else {
// This is the original version of the code.
//
// The above patched statement following will do
// one of the following three things:
//
// 1. If the execution is not Replaying, the execution
// won't get here because the first patched statement
// will be True.
// 2. If the execution is Replaying, and the original
// run had a patch marker v3 or v2 at this location in the event
// history, the execution
// won't get here because the first or second patched statement
// will be True (respectively).
// 3. If the execution is Replaying, and condition 2
// doesn't hold, then it will run this code.
}
```
<!--SNIPEND-->

To add more clarity, the following sample shows how `patched()` will behave in a different conditional block.
In this case, the code's conditional block doesn't have the newest code at the top.
Because `patched()` will always return `true` when not Replaying, this snippet will run the `v2` branch instead of `v3` in new executions.

<!--SNIPSTART typescript-patching-anti-example-->

```ts
if (patched('v2')) {
// This is bad because when doing an original execution (i.e. not replaying),
// all patched statements evaluate to True (and put a marker
// in the event history), which means that new executions
// will use v2, and miss v3 below
}
else if (patched('v3')) {}
else {}
```

The moral of the story is that when not Replaying, `patched()` will return true and write the patch ID to the Event History.
And when Replaying, it will only return true if the patch ID matches that in the Event History.

<!--SNIPEND-->

#### Step 1: Patch in new code

`patched` inserts a marker into the Workflow history.
Expand Down Expand Up @@ -238,6 +322,14 @@ export async function myWorkflow(): Promise<void> {

`vFinal` is safe to deploy once all `v2` or earlier Workflows are complete due to the assertion mentioned above.

### Best Practice of Using TypeScript Objects as Arguments and Returns

As a side note on the Patching API, its behavior is why Temporal recommends using single objects as arguments and returns from Signals, Queries, Updates, and Activities, rather than using multiple arguments.
The Patching API's main use case is to support branching in an `if` block of a function body.
It is not designed to be used to set different functions or function signatures for different Workflow Versions.

Because of this, Temporal recommends that each Signal, Activity, etc, accepts a single object and returns a single object, so the function signature can stay constant, and you can do your versioning logic using `patched()` within the function body.

### Upgrading Workflow dependencies

Upgrading Workflow dependencies (such as ones installed into `node_modules`) _might_ break determinism in unpredictable ways.
Expand Down