diff --git a/docs/develop/dotnet/versioning.mdx b/docs/develop/dotnet/versioning.mdx index 8b02cb5e54..83ea7f264b 100644 --- a/docs/develop/dotnet/versioning.mdx +++ b/docs/develop/dotnet/versioning.mdx @@ -183,3 +183,164 @@ public class MyWorkflow } } ``` + +### Detailed Description of the Patched Function + +We take a deep dive into the behavior of the `patched()` function in this optional 37 minute YouTube series: + +
+ +
+ +#### Behavior When Not Replaying + +If not replaying, and the execution hits a call to patched, it first checks the event history, and: + +- If the patch ID is not in the event history, it will add a marker to the event history, upsert a search attribute, and return true. + This happens in a given patch ID's first block. +- If the patch ID is in the event history, it won't modify the history, and it will return true. + This happens in a given patch ID's subsequent blocks. + +> There is a caveat to the above, and we will discuss that below. + +#### Behavior When Replaying With Marker Before-Or-At Current Location + +If Replaying: + +- If the code has a call to patched, and if the event history + has a marker from a call to patched in the same place (which means it + will match the original event history), then + it writes a marker to the replay event history and returns true. + *This is similar to the behavior of the non-replay case, and + just like in that case, this happens in a given patch ID's first block* +- If the code has a call to patched, and the event history + has a marker with that Patch ID earlier in the history, + then it will simply return true and not modify the + replay event history. + *This is similar to the behavior of the non-replay case, and + just like in that case, this happens in a given patch ID's subsequent blocks* + +#### Behavior When Replaying With Marker After Current Location + +If the Marker Event is after where the execution currently is +in the event history, then, in other words, +the patch is before the original patch, then the patch is too early. It will +attempt to write the marker to the replay event +history, but it will throw a non-deterministic +exception because the replay and original event +histories don't match + +#### Behavior When Replaying With No Marker For that Patch ID + +It will return false and not add anything to +the event history. Furthermore, ***and this is the +caveat mentioned in the preceeding section [Behavior When Not Replaying](#behavior-when-not-replaying)***, it will make all future calls to patched +with that ID false -- even after it is done replaying +and is running new code. + +Why is this a caveat? + +In the [preceding section](#behavior-when-not-replaying) where we discussed the behavior when not replaying , we said that if not replaying, +the patched function will always return true, and if +the marker doesn't exist, it will add it, and if +the marker already exists, it won't re-add it. + +But what this +is saying is that this doesn't hold if there was already +a call to patched with that ID in the replay code, but not +in the event history. In this situation, it won't return +true. + +#### A Summary of the Two Potentially Unexpected Behaviors + +1. When Replaying, in the scenario of ***it hits a call to + patched, but that patch ID isn't before/on that point in + the event history***, you may not expect that + the event history *after* where you currently + are matters. Because: + 1. If that patch ID exists later, you get an NDE [(see above: Behavior When Replaying With Marker After Current Location)](#behavior-when-replaying-with-marker-after-current-location). + 2. If it doesn't exist later, you don't get an NDE, and + it returns false + [(see above: Behavior When Replaying With No Marker For that Patch ID)](#behavior-when-replaying-with-no-marker-for-that-patch-id). + +2. When Replaying, if you hit a call to patched with an ID that + doesn't exist in the history, then not only will it return + false in that occurence, but it will also return false if + the execution surpasses the Replay threshold and is running new code. + [(see above: Behavior When Replaying With No Marker For that Patch ID)](#behavior-when-replaying-with-no-marker-for-that-patch-id). + +#### Implications of the Behaviors + +If you deploy new code while a worker is down, +any workflows that were in the middle of executing will replay +using old code and then for the rest of the execution, they +will either: + +1. Use new code if there was no call to patched in the replay code +2. If there was a call to patched in the replay code, they will + run the non-patched code during and after replay + +This might sound odd, but it's actually exactly what's needed because +that means that if the future patched code depends on earlier patched code, +then it won't use the new code -- it will use the old code. But if +there's new code in the future, and there was no code earlier in the +body that required the new patch, then it can switch over to the new code, +and it will do that. + +Note that this behavior means that the Workflow ***does not always run +the newest code***. It only does that if not replaying or if +surpassed replay and there hasn't been a call to patched (with that ID) throughout +the replay. + +#### Recommendations + +Based on this behavior and the implications, when patching in new code, always put the newest code at the top of an if-patched-block. + + + +```csharp +if (patched('v3')) { + // This is the newest version of the code. + // put this at the top, so when it is running + // a fresh execution and not replaying, + // this patched statement will return true + // and it will run the new code. +} else if (patched('v2')) { +} else { +} + ``` + + +The following sample shows how `patched()` will behave in a conditional block that's arranged differently. +In this case, the code's conditional block doesn't have the newest code at the top. +Because `patched()` will return `true` when not Replaying (except with the preceding caveats), this snippet will run the `v2` branch instead of `v3` in new executions. + + + +```csharp +if (patched('v2')) { + // This is bad because when doing a new execution (i.e. not replaying), + // 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 {} +``` + + + + +### 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. diff --git a/docs/develop/python/versioning.mdx b/docs/develop/python/versioning.mdx index 86519e7baa..25e932f5b5 100644 --- a/docs/develop/python/versioning.mdx +++ b/docs/develop/python/versioning.mdx @@ -4,7 +4,7 @@ title: Versioning - Python SDK sidebar_label: Versioning description: Learn how to ensure deterministic Temporal Workflow execution and safely deploy updates using the Python SDK's patching and Worker Versioning APIs, for scalable long-running Workflows. slug: /develop/python/versioning -toc_max_heading_level: 2 +toc_max_heading_level: 4 keywords: - best practices - code sample @@ -35,7 +35,17 @@ a non-deterministic issue if not handled correctly. ## Introduction to Versioning -Because we design for potentially long running Workflows at scale, versioning with Temporal works differently. We explain more in this optional 30 minute introduction: [https://www.youtube.com/watch?v=kkP899WxgzY](https://www.youtube.com/watch?v=kkP899WxgzY) +Because we design for potentially long running Workflows at scale, versioning with Temporal works differently. We explain more in this optional 30 minute introduction: + +
+ +
## How to use the Python SDK Patching API {#python-sdk-patching-api} @@ -115,7 +125,7 @@ Implementing patching involves three steps: ### 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) @@ -199,6 +209,172 @@ class MyWorkflow: ) ``` + +### Detailed Description of the Patched Function + +We take a deep dive into the behavior of the `patched()` function in this optional 37 minute YouTube series: + +
+ +
+ +#### Behavior When Not Replaying + +If not replaying, and the execution hits a call to patched, it first checks the event history, and: + +- If the patch ID is not in the event history, it will add a marker to the event history, upsert a search attribute, and return true. + This happens in a given patch ID's first block. +- If the patch ID is in the event history, it won't modify the history, and it will return true. + This happens in a given patch ID's subsequent blocks. + +> There is a caveat to the above, and we will discuss that below. + +#### Behavior When Replaying With Marker Before-Or-At Current Location + +If Replaying: + +- If the code has a call to patched, and if the event history + has a marker from a call to patched in the same place (which means it + will match the original event history), then + it writes a marker to the replay event history and returns true. + *This is similar to the behavior of the non-replay case, and + just like in that case, this happens in a given patch ID's first block* +- If the code has a call to patched, and the event history + has a marker with that Patch ID earlier in the history, + then it will simply return true and not modify the + replay event history. + *This is similar to the behavior of the non-replay case, and + just like in that case, this happens in a given patch ID's subsequent blocks* + +#### Behavior When Replaying With Marker After Current Location + +If the Marker Event is after where the execution currently is +in the event history, then, in other words, +the patch is before the original patch, then the patch is too early. It will +attempt to write the marker to the replay event +history, but it will throw a non-deterministic +exception because the replay and original event +histories don't match + +#### Behavior When Replaying With No Marker For that Patch ID + +It will return false and not add anything to +the event history. Furthermore, ***and this is the +caveat mentioned in the preceeding section [Behavior When Not Replaying](#behavior-when-not-replaying)***, it will make all future calls to patched +with that ID false -- even after it is done replaying +and is running new code. + +Why is this a caveat? + +In the [preceding section](#behavior-when-not-replaying) where we discussed the behavior when not replaying , we said that if not replaying, +the patched function will always return true, and if +the marker doesn't exist, it will add it, and if +the marker already exists, it won't re-add it. + +But what this +is saying is that this doesn't hold if there was already +a call to patched with that ID in the replay code, but not +in the event history. In this situation, it won't return +true. + +#### A Summary of the Two Potentially Unexpected Behaviors + +1. When Replaying, in the scenario of ***it hits a call to + patched, but that patch ID isn't before/on that point in + the event history***, you may not expect that + the event history *after* where you currently + are matters. Because: + 1. If that patch ID exists later, you get an NDE [(see above: Behavior When Replaying With Marker After Current Location)](#behavior-when-replaying-with-marker-after-current-location). + 2. If it doesn't exist later, you don't get an NDE, and + it returns false + [(see above: Behavior When Replaying With No Marker For that Patch ID)](#behavior-when-replaying-with-no-marker-for-that-patch-id). + +2. When Replaying, if you hit a call to patched with an ID that + doesn't exist in the history, then not only will it return + false in that occurence, but it will also return false if + the execution surpasses the Replay threshold and is running new code. + [(see above: Behavior When Replaying With No Marker For that Patch ID)](#behavior-when-replaying-with-no-marker-for-that-patch-id). + +#### Implications of the Behaviors + +If you deploy new code while a worker is down, +any workflows that were in the middle of executing will replay +using old code and then for the rest of the execution, they +will either: + +1. Use new code if there was no call to patched in the replay code +2. If there was a call to patched in the replay code, they will + run the non-patched code during and after replay + +This might sound odd, but it's actually exactly what's needed because +that means that if the future patched code depends on earlier patched code, +then it won't use the new code -- it will use the old code. But if +there's new code in the future, and there was no code earlier in the +body that required the new patch, then it can switch over to the new code, +and it will do that. + +Note that this behavior means that the Workflow ***does not always run +the newest code***. It only does that if not replaying or if +surpassed replay and there hasn't been a call to patched (with that ID) throughout +the replay. + +#### Recommendations + +Based on this behavior and the implications, when patching in new code, always put the newest code at the top of an if-patched-block. + + + +```python +if patched('v3'): + # This is the newest version of the code. + # put this at the top, so when it is running + # a fresh execution and not replaying, + # this patched statement will return true + # and it will run the new code. + pass +elif patched('v2'): + pass +else: + pass + ``` + + +The following sample shows how `patched()` will behave in a conditional block that's arranged differently. +In this case, the code's conditional block doesn't have the newest code at the top. +Because `patched()` will return `True` when not Replaying (except with the preceding caveats), this snippet will run the `v2` branch instead of `v3` in new executions. + + + +```python +if patched('v2'): + # This is bad because when doing a new execution (i.e. not replaying), + # 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 +``` + + + + +### 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 diff --git a/docs/develop/typescript/versioning.mdx b/docs/develop/typescript/versioning.mdx index 406b4e7f8e..cc227d4038 100644 --- a/docs/develop/typescript/versioning.mdx +++ b/docs/develop/typescript/versioning.mdx @@ -238,6 +238,125 @@ export async function myWorkflow(): Promise { `vFinal` is safe to deploy once all `v2` or earlier Workflows are complete due to the assertion mentioned above. + +#### Detailed Description of the Patched Function + +Here is a detailed explanation of how the `patched()` function behaves. + +##### Behavior When Not Replaying + +If not replaying, and the execution hits a call to patched, it first checks the event history, and: + +- If the patch ID is not in the event history, it will add a marker to the event history, upsert a search attribute, and return true. + This happens in a given patch ID's first block. +- If the patch ID is in the event history, it won't modify the history, and it will return true. + This happens in a given patch ID's subsequent blocks. + +##### Behavior When Replaying With Marker Before-Or-At Current Location + +If Replaying: + +- If the code has a call to patched, and if the event history + has a marker from a call to patched in the same place (which means it + will match the original event history), then + it writes a marker to the replay event history and returns true. + *This is similar to the behavior of the non-replay case, and + just like in that case, this happens in a given patch ID's first block* +- If the code has a call to patched, and the event history + has a marker with that Patch ID earlier in the history, + then it will simply return true and not modify the + replay event history. + *This is similar to the behavior of the non-replay case, and + just like in that case, this happens in a given patch ID's subsequent blocks* +- If the code has a call to patched, and there no marker on or before that spot in the execution, it returns false. + +##### Implications of the Behaviors + + +If you deploy new code, it will run the new code if it is +not replaying, and if it is replaying, it will just do what +it did the previous time. + +This means that if it has gotten through some of your code, then +you stop the worker and deploy new code, then when it replays, +it will use the old code throughout the replay, but switch over +to new code after it has passed the replay threshold. This means +your new code and your old code must work together. For example, +if your Workflow Definition originally looked like this: + +```ts +console.log('original code before the sleep') +await sleep(10000); // <-- Stop the Worker while this is waiting, and deploy the new code below +console.log('original code after the sleep') +``` + +Now we stop the Worker during the sleep, and wrap our original +code in the else part of a patched `if` statement, and start +our Worker again. + +```ts +if (patched('my-change-id')) { + console.log('new code before the sleep') +} else { + console.log('original code before the sleep') // this will run +} +await sleep(10000); +if (patched('my-change-id')) { + console.log('new code after the sleep') // this will run +} else { + console.log('original code after the sleep') +} +``` + +In the first part, it will be Replaying, and it will run the old code, +and after the sleep, it won't be Replaying, and it will run the new code. + +##### Recommendations + +Based on this behavior and the implications, when patching in new code, always put the newest code at the top of an if-patched-block. + + + +```ts +if (patched('v3')) { + // This is the newest version of the code. + // put this at the top, so when it is running + // a fresh execution and not replaying, + // this patched statement will return true + // and it will run the new code. +} else if (patched('v2')) { +} else { +} + ``` + + +The following sample shows how `patched()` will behave in a conditional block that's arranged differently. +In this case, the code's conditional block doesn't have the newest code at the top. +Because `patched()` will return `true` when not Replaying (except with the preceding caveats), this snippet will run the `v2` branch instead of `v3` in new executions. + + + +```ts +if (patched('v2')) { + // This is bad because when doing a new execution (i.e. not replaying), + // 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 {} +``` + + + +### 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.