From 55f44b435c657fed5d4d8f1ce2da4d3e2edceffd Mon Sep 17 00:00:00 2001 From: ktenzer Date: Mon, 27 Feb 2023 14:16:54 -0800 Subject: [PATCH 01/10] Added sample for demonstrating how to patch a workflow Signed-off-by: ktenzer --- hello/hello_patched.py | 114 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 hello/hello_patched.py diff --git a/hello/hello_patched.py b/hello/hello_patched.py new file mode 100644 index 00000000..90ceaa89 --- /dev/null +++ b/hello/hello_patched.py @@ -0,0 +1,114 @@ +import asyncio +import logging +from dataclasses import dataclass +from datetime import timedelta + +from temporalio import activity, workflow, exceptions +from temporalio.client import Client +from temporalio.worker import Worker + + +# While we could use multiple parameters in the activity, Temporal strongly +# encourages using a single dataclass instead which can have fields added to it +# in a backwards-compatible way. +@dataclass +class ComposeGreetingInput: + greeting: str + name: str + + +# Basic activity that logs and does string concatenation +@activity.defn +async def compose_greeting(input: ComposeGreetingInput) -> str: + activity.logger.info("Running activity with parameter %s" % input) + return f"{input.greeting}, {input.name}!" + +async def timer(time: int) -> int: + asyncio.sleep(time) + + +# Basic workflow that execute a activity and fires a timer. Demonstrates how to version a workflow using patched. +# Steps +# 1) First run the workflow +# 2) Ctrl-C and interrupt workflow +# 3) Comment the current async run function and comment out the async run function with patch (this simulates patching a running workflow) +# 4) Re-run the workflow, it will detect a workflow is running and pickup the execution following the old code path +# 5) Re-run the workflow again, since a workflow isn't running it will pickup execution following the new code path + +@workflow.defn +class PatchedWorkflow: + @workflow.run + async def run(self, name: str) -> str: + workflow.logger.info("Running workflow with parameter %s" % name) + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Hello", name), + start_to_close_timeout=timedelta(seconds=60), + ) + await asyncio.sleep(120) + return greeting + + +# async def run(self, name: str) -> str: +# if workflow.patched('my-patch-v1'): +# print(f"Running new version of workflow") +# workflow.logger.info("Running new v1 workflow with parameter %s" % name) +# greeting = await workflow.execute_activity( +# compose_greeting, +# ComposeGreetingInput("Hello-v1", name), +# start_to_close_timeout=timedelta(seconds=60), +# ) +# await asyncio.sleep(120) +# return greeting +# else: +# print(f"Running old version of workflow") +# workflow.logger.info("Running original workflow with parameter %s" % name) +# greeting = await workflow.execute_activity( +# compose_greeting, +# ComposeGreetingInput("Hello", name), +# start_to_close_timeout=timedelta(seconds=60), +# ) +# await asyncio.sleep(120) +# return greeting + +async def main(): + # Uncomment the line below to see logging + # logging.basicConfig(level=logging.INFO) + + # Start client + client = await Client.connect("localhost:7233") + + # Run a worker for the workflow + async with Worker( + client, + task_queue="hello-patched-task-queue", + workflows=[PatchedWorkflow], + activities=[compose_greeting], + ): + + # While the worker is running, use the client to run the workflow and + # print out its result. Check if the workflow is already running and if so + # wait for it to complete. Note, in many production setups, the client + # would be in a completely separate process from the worker. + try: + result = await client.execute_workflow( + PatchedWorkflow.run, + "World", + id="hello-patched-workflow-id", + task_queue="hello-patched-task-queue", + ) + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + workflow_result = await client.get_workflow_handle("hello-patched-workflow-id").result() + print(f"Successful workflow result: {workflow_result}") + + # while(True): + # await asyncio.sleep(30) + # print(f"Working waiting on tasks...") + + + + +if __name__ == "__main__": + asyncio.run(main()) From 86f1635255d29c9fc7df246be4022398d4b84036 Mon Sep 17 00:00:00 2001 From: ktenzer Date: Mon, 27 Feb 2023 14:26:44 -0800 Subject: [PATCH 02/10] Linted patch sample Signed-off-by: ktenzer --- hello/hello_patched.py | 45 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/hello/hello_patched.py b/hello/hello_patched.py index 90ceaa89..efd3825f 100644 --- a/hello/hello_patched.py +++ b/hello/hello_patched.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from datetime import timedelta -from temporalio import activity, workflow, exceptions +from temporalio import activity, exceptions, workflow from temporalio.client import Client from temporalio.worker import Worker @@ -23,11 +23,12 @@ async def compose_greeting(input: ComposeGreetingInput) -> str: activity.logger.info("Running activity with parameter %s" % input) return f"{input.greeting}, {input.name}!" + async def timer(time: int) -> int: - asyncio.sleep(time) + asyncio.sleep(time) -# Basic workflow that execute a activity and fires a timer. Demonstrates how to version a workflow using patched. +# Basic workflow that execute a activity and fires a timer. Demonstrates how to version a workflow using patched. # Steps # 1) First run the workflow # 2) Ctrl-C and interrupt workflow @@ -35,33 +36,34 @@ async def timer(time: int) -> int: # 4) Re-run the workflow, it will detect a workflow is running and pickup the execution following the old code path # 5) Re-run the workflow again, since a workflow isn't running it will pickup execution following the new code path + @workflow.defn class PatchedWorkflow: @workflow.run async def run(self, name: str) -> str: - workflow.logger.info("Running workflow with parameter %s" % name) - greeting = await workflow.execute_activity( + workflow.logger.info("Running workflow with parameter %s" % name) + greeting = await workflow.execute_activity( compose_greeting, ComposeGreetingInput("Hello", name), start_to_close_timeout=timedelta(seconds=60), - ) - await asyncio.sleep(120) - return greeting - - + ) + await asyncio.sleep(120) + return greeting + + # async def run(self, name: str) -> str: # if workflow.patched('my-patch-v1'): -# print(f"Running new version of workflow") +# print(f"Running new version of workflow") # workflow.logger.info("Running new v1 workflow with parameter %s" % name) # greeting = await workflow.execute_activity( # compose_greeting, # ComposeGreetingInput("Hello-v1", name), # start_to_close_timeout=timedelta(seconds=60), # ) -# await asyncio.sleep(120) +# await asyncio.sleep(120) # return greeting # else: -# print(f"Running old version of workflow") +# print(f"Running old version of workflow") # workflow.logger.info("Running original workflow with parameter %s" % name) # greeting = await workflow.execute_activity( # compose_greeting, @@ -69,7 +71,8 @@ async def run(self, name: str) -> str: # start_to_close_timeout=timedelta(seconds=60), # ) # await asyncio.sleep(120) -# return greeting +# return greeting + async def main(): # Uncomment the line below to see logging @@ -98,16 +101,16 @@ async def main(): task_queue="hello-patched-task-queue", ) print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: + except exceptions.WorkflowAlreadyStartedError: print(f"Workflow already running") - workflow_result = await client.get_workflow_handle("hello-patched-workflow-id").result() + workflow_result = await client.get_workflow_handle( + "hello-patched-workflow-id" + ).result() print(f"Successful workflow result: {workflow_result}") - - # while(True): - # await asyncio.sleep(30) - # print(f"Working waiting on tasks...") - + # while(True): + # await asyncio.sleep(30) + # print(f"Working waiting on tasks...") if __name__ == "__main__": From 63258ff97a364334141c3c557f75c2cce2849a15 Mon Sep 17 00:00:00 2001 From: ktenzer Date: Mon, 27 Feb 2023 14:31:43 -0800 Subject: [PATCH 03/10] fixed lint issue in patch sample Signed-off-by: ktenzer --- hello/hello_patched.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hello/hello_patched.py b/hello/hello_patched.py index efd3825f..79935226 100644 --- a/hello/hello_patched.py +++ b/hello/hello_patched.py @@ -24,10 +24,6 @@ async def compose_greeting(input: ComposeGreetingInput) -> str: return f"{input.greeting}, {input.name}!" -async def timer(time: int) -> int: - asyncio.sleep(time) - - # Basic workflow that execute a activity and fires a timer. Demonstrates how to version a workflow using patched. # Steps # 1) First run the workflow From 85046644212b4d4e6f779a2e80697cac140c047d Mon Sep 17 00:00:00 2001 From: ktenzer Date: Mon, 27 Feb 2023 15:20:08 -0800 Subject: [PATCH 04/10] broke out the different versions into their own files and added readme Signed-off-by: ktenzer --- hello/patch/README.md | 9 ++++ hello/patch/hello_patch-v1.py | 73 +++++++++++++++++++++++++++++ hello/patch/hello_patch-v2.py | 86 +++++++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 hello/patch/README.md create mode 100644 hello/patch/hello_patch-v1.py create mode 100644 hello/patch/hello_patch-v2.py diff --git a/hello/patch/README.md b/hello/patch/README.md new file mode 100644 index 00000000..6d7a9eb2 --- /dev/null +++ b/hello/patch/README.md @@ -0,0 +1,9 @@ +# Patch Workflow Sample +This sample will demonstrate how to version a workflow. The sample workflow will execute a activity and fire a timer. + +- First run the v1 workflow +- Ctrl-C and interrupt workflow +- Run the v2 workflow + - The workflow will is running, execution resumes (after timer fires) and your v1 code is run +- Re-run the v2 workflow + - The workflow will execute your new v2 code diff --git a/hello/patch/hello_patch-v1.py b/hello/patch/hello_patch-v1.py new file mode 100644 index 00000000..5640ab04 --- /dev/null +++ b/hello/patch/hello_patch-v1.py @@ -0,0 +1,73 @@ +import asyncio +import logging +from dataclasses import dataclass +from datetime import timedelta + +from temporalio import activity, exceptions, workflow +from temporalio.client import Client +from temporalio.worker import Worker + + +# While we could use multiple parameters in the activity, Temporal strongly +# encourages using a single dataclass instead which can have fields added to it +# in a backwards-compatible way. +@dataclass +class ComposeGreetingInput: + greeting: str + name: str + +# Basic activity that logs and does string concatenation +@activity.defn +async def compose_greeting(input: ComposeGreetingInput) -> str: + activity.logger.info("Running activity with parameter %s" % input) + return f"{input.greeting}, {input.name}!" + +@workflow.defn +class PatchedWorkflow: + @workflow.run + async def run(self, name: str) -> str: + workflow.logger.info("Running workflow with parameter %s" % name) + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Hello", name), + start_to_close_timeout=timedelta(seconds=70), + ) + await asyncio.sleep(60) + return greeting + +async def main(): + # Uncomment the line below to see logging + # logging.basicConfig(level=logging.INFO) + + # Start client + client = await Client.connect("localhost:7233") + + # Run a worker for the workflow + async with Worker( + client, + task_queue="hello-patched-task-queue", + workflows=[PatchedWorkflow], + activities=[compose_greeting], + ): + + # While the worker is running, use the client to run the workflow and + # print out its result. Check if the workflow is already running and if so + # wait for it to complete. Note, in many production setups, the client + # would be in a completely separate process from the worker. + try: + result = await client.execute_workflow( + PatchedWorkflow.run, + "World", + id="hello-patched-workflow-id", + task_queue="hello-patched-task-queue", + ) + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + workflow_result = await client.get_workflow_handle( + "hello-patched-workflow-id" + ).result() + print(f"Successful workflow result: {workflow_result}") + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/hello/patch/hello_patch-v2.py b/hello/patch/hello_patch-v2.py new file mode 100644 index 00000000..4bfdbf43 --- /dev/null +++ b/hello/patch/hello_patch-v2.py @@ -0,0 +1,86 @@ +import asyncio +import logging +from dataclasses import dataclass +from datetime import timedelta + +from temporalio import activity, exceptions, workflow +from temporalio.client import Client +from temporalio.worker import Worker + + +# While we could use multiple parameters in the activity, Temporal strongly +# encourages using a single dataclass instead which can have fields added to it +# in a backwards-compatible way. +@dataclass +class ComposeGreetingInput: + greeting: str + name: str + +# Basic activity that logs and does string concatenation +@activity.defn +async def compose_greeting(input: ComposeGreetingInput) -> str: + activity.logger.info("Running activity with parameter %s" % input) + return f"{input.greeting}, {input.name}!" + +@workflow.defn +class PatchedWorkflow: + @workflow.run + async def run(self, name: str) -> str: + if workflow.patched('my-patch-v2'): + print(f"Running new version of workflow") + workflow.logger.info("Running new v1 workflow with parameter %s" % name) + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Hello-v2", name), + start_to_close_timeout=timedelta(seconds=70), + ) + await asyncio.sleep(60) + return greeting + else: + print(f"Running old version of workflow") + workflow.logger.info("Running original workflow with parameter %s" % name) + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Hello", name), + start_to_close_timeout=timedelta(seconds=70), + ) + await asyncio.sleep(60) + return greeting + + +async def main(): + # Uncomment the line below to see logging + # logging.basicConfig(level=logging.INFO) + + # Start client + client = await Client.connect("localhost:7233") + + # Run a worker for the workflow + async with Worker( + client, + task_queue="hello-patched-task-queue", + workflows=[PatchedWorkflow], + activities=[compose_greeting], + ): + + # While the worker is running, use the client to run the workflow and + # print out its result. Check if the workflow is already running and if so + # wait for it to complete. Note, in many production setups, the client + # would be in a completely separate process from the worker. + try: + result = await client.execute_workflow( + PatchedWorkflow.run, + "World", + id="hello-patched-workflow-id", + task_queue="hello-patched-task-queue", + ) + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + workflow_result = await client.get_workflow_handle( + "hello-patched-workflow-id" + ).result() + print(f"Successful workflow result: {workflow_result}") + +if __name__ == "__main__": + asyncio.run(main()) From 70aaf6fa6a8a8b6ef92f0918e6c491203b9cf5e1 Mon Sep 17 00:00:00 2001 From: ktenzer Date: Mon, 27 Feb 2023 15:21:30 -0800 Subject: [PATCH 05/10] deleting original patched sample Signed-off-by: ktenzer --- hello/hello_patched.py | 113 ----------------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 hello/hello_patched.py diff --git a/hello/hello_patched.py b/hello/hello_patched.py deleted file mode 100644 index 79935226..00000000 --- a/hello/hello_patched.py +++ /dev/null @@ -1,113 +0,0 @@ -import asyncio -import logging -from dataclasses import dataclass -from datetime import timedelta - -from temporalio import activity, exceptions, workflow -from temporalio.client import Client -from temporalio.worker import Worker - - -# While we could use multiple parameters in the activity, Temporal strongly -# encourages using a single dataclass instead which can have fields added to it -# in a backwards-compatible way. -@dataclass -class ComposeGreetingInput: - greeting: str - name: str - - -# Basic activity that logs and does string concatenation -@activity.defn -async def compose_greeting(input: ComposeGreetingInput) -> str: - activity.logger.info("Running activity with parameter %s" % input) - return f"{input.greeting}, {input.name}!" - - -# Basic workflow that execute a activity and fires a timer. Demonstrates how to version a workflow using patched. -# Steps -# 1) First run the workflow -# 2) Ctrl-C and interrupt workflow -# 3) Comment the current async run function and comment out the async run function with patch (this simulates patching a running workflow) -# 4) Re-run the workflow, it will detect a workflow is running and pickup the execution following the old code path -# 5) Re-run the workflow again, since a workflow isn't running it will pickup execution following the new code path - - -@workflow.defn -class PatchedWorkflow: - @workflow.run - async def run(self, name: str) -> str: - workflow.logger.info("Running workflow with parameter %s" % name) - greeting = await workflow.execute_activity( - compose_greeting, - ComposeGreetingInput("Hello", name), - start_to_close_timeout=timedelta(seconds=60), - ) - await asyncio.sleep(120) - return greeting - - -# async def run(self, name: str) -> str: -# if workflow.patched('my-patch-v1'): -# print(f"Running new version of workflow") -# workflow.logger.info("Running new v1 workflow with parameter %s" % name) -# greeting = await workflow.execute_activity( -# compose_greeting, -# ComposeGreetingInput("Hello-v1", name), -# start_to_close_timeout=timedelta(seconds=60), -# ) -# await asyncio.sleep(120) -# return greeting -# else: -# print(f"Running old version of workflow") -# workflow.logger.info("Running original workflow with parameter %s" % name) -# greeting = await workflow.execute_activity( -# compose_greeting, -# ComposeGreetingInput("Hello", name), -# start_to_close_timeout=timedelta(seconds=60), -# ) -# await asyncio.sleep(120) -# return greeting - - -async def main(): - # Uncomment the line below to see logging - # logging.basicConfig(level=logging.INFO) - - # Start client - client = await Client.connect("localhost:7233") - - # Run a worker for the workflow - async with Worker( - client, - task_queue="hello-patched-task-queue", - workflows=[PatchedWorkflow], - activities=[compose_greeting], - ): - - # While the worker is running, use the client to run the workflow and - # print out its result. Check if the workflow is already running and if so - # wait for it to complete. Note, in many production setups, the client - # would be in a completely separate process from the worker. - try: - result = await client.execute_workflow( - PatchedWorkflow.run, - "World", - id="hello-patched-workflow-id", - task_queue="hello-patched-task-queue", - ) - print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: - print(f"Workflow already running") - workflow_result = await client.get_workflow_handle( - "hello-patched-workflow-id" - ).result() - print(f"Successful workflow result: {workflow_result}") - - # while(True): - # await asyncio.sleep(30) - # print(f"Working waiting on tasks...") - - -if __name__ == "__main__": - asyncio.run(main()) From 0b1e06795d0b56a9075dd9c8da9b4ccfd6910901 Mon Sep 17 00:00:00 2001 From: ktenzer Date: Mon, 27 Feb 2023 15:22:47 -0800 Subject: [PATCH 06/10] fixing lint issues (again) Signed-off-by: ktenzer --- hello/patch/hello_patch-v1.py | 4 ++++ hello/patch/hello_patch-v2.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/hello/patch/hello_patch-v1.py b/hello/patch/hello_patch-v1.py index 5640ab04..4e0ce2c1 100644 --- a/hello/patch/hello_patch-v1.py +++ b/hello/patch/hello_patch-v1.py @@ -16,12 +16,14 @@ class ComposeGreetingInput: greeting: str name: str + # Basic activity that logs and does string concatenation @activity.defn async def compose_greeting(input: ComposeGreetingInput) -> str: activity.logger.info("Running activity with parameter %s" % input) return f"{input.greeting}, {input.name}!" + @workflow.defn class PatchedWorkflow: @workflow.run @@ -35,6 +37,7 @@ async def run(self, name: str) -> str: await asyncio.sleep(60) return greeting + async def main(): # Uncomment the line below to see logging # logging.basicConfig(level=logging.INFO) @@ -69,5 +72,6 @@ async def main(): ).result() print(f"Successful workflow result: {workflow_result}") + if __name__ == "__main__": asyncio.run(main()) diff --git a/hello/patch/hello_patch-v2.py b/hello/patch/hello_patch-v2.py index 4bfdbf43..69747269 100644 --- a/hello/patch/hello_patch-v2.py +++ b/hello/patch/hello_patch-v2.py @@ -16,17 +16,19 @@ class ComposeGreetingInput: greeting: str name: str + # Basic activity that logs and does string concatenation @activity.defn async def compose_greeting(input: ComposeGreetingInput) -> str: activity.logger.info("Running activity with parameter %s" % input) return f"{input.greeting}, {input.name}!" + @workflow.defn class PatchedWorkflow: @workflow.run async def run(self, name: str) -> str: - if workflow.patched('my-patch-v2'): + if workflow.patched("my-patch-v2"): print(f"Running new version of workflow") workflow.logger.info("Running new v1 workflow with parameter %s" % name) greeting = await workflow.execute_activity( @@ -82,5 +84,6 @@ async def main(): ).result() print(f"Successful workflow result: {workflow_result}") + if __name__ == "__main__": asyncio.run(main()) From 18911e4d1924d20c34898888f183ce012bc17487 Mon Sep 17 00:00:00 2001 From: ktenzer Date: Tue, 28 Feb 2023 14:17:36 -0800 Subject: [PATCH 07/10] re-implemented v1 and v2 of workflow using args Signed-off-by: ktenzer --- hello/hello_patch.py | 120 ++++++++++++++++++++++++++++++++++ hello/patch/README.md | 9 --- hello/patch/hello_patch-v1.py | 77 ---------------------- hello/patch/hello_patch-v2.py | 89 ------------------------- 4 files changed, 120 insertions(+), 175 deletions(-) create mode 100644 hello/hello_patch.py delete mode 100644 hello/patch/README.md delete mode 100644 hello/patch/hello_patch-v1.py delete mode 100644 hello/patch/hello_patch-v2.py diff --git a/hello/hello_patch.py b/hello/hello_patch.py new file mode 100644 index 00000000..96c20c41 --- /dev/null +++ b/hello/hello_patch.py @@ -0,0 +1,120 @@ +import asyncio +import logging +import sys +from dataclasses import dataclass +from datetime import timedelta + +from temporalio import activity, exceptions, workflow +from temporalio.client import Client +from temporalio.worker import Worker + + +# While we could use multiple parameters in the activity, Temporal strongly +# encourages using a single dataclass instead which can have fields added to it +# in a backwards-compatible way. +@dataclass +class ComposeGreetingInput: + greeting = "Hello" + name: str + + +# Basic activity that logs and does string concatenation +@activity.defn +async def compose_greeting(input: ComposeGreetingInput) -> str: + activity.logger.info("Running activity with parameter %s" % input) + return f"{input.greeting}, {input.name}!" + + +# Depending on version argument passed in, will execute a different workflow. +# The v1 workflow shows the original workflow with a single activity that +# outputs "Hello, World". If we wanted to change this workflow without breaking +# already running workflows, we can use patched. The v2 workflow shows how to +# use patched to continue to output "Hello, World" for already running workflows +# and output "Hello, Universe" for newly started workflows. The timer (sleep) exists +# to allow experimentation, around how changes affect running workflows. +@workflow.defn +class PatchWorkflow: + @workflow.run + async def run(self, version: str) -> str: + greeting = "" + if version == "v1": + print(f"Running workflow version {version}") + workflow.logger.info("Running workflow version {version}") + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("World"), + start_to_close_timeout=timedelta(seconds=70), + ) + + await asyncio.sleep(60) + elif version == "v2": + print(f"Running workflow version {version}") + workflow.logger.info("Running workflow version {version}") + if workflow.patched("my-patch-v2"): + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Universe"), + start_to_close_timeout=timedelta(seconds=70), + ) + + await asyncio.sleep(60) + else: + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("World"), + start_to_close_timeout=timedelta(seconds=70), + ) + + await asyncio.sleep(60) + return greeting + + +async def main(): + # Check arguments and ensure either v1 or v2 is passed in + if len(sys.argv) > 2: + print(f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2") + exit() + if len(sys.argv) <= 1: + print(f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2") + exit() + if sys.argv[1] != "v1" and sys.argv[1] != "v2": + print(f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2") + exit() + + version = sys.argv[1] + + # Uncomment the line below to see logging + # logging.basicConfig(level=logging.INFO) + + # Start client + client = await Client.connect("localhost:7233") + + # Run a worker for the workflow + async with Worker( + client, + task_queue="hello-patch-task-queue", + workflows=[PatchWorkflow], + activities=[compose_greeting], + ): + # While the worker is running, use the client to run the workflow and + # print out its result. Check if the workflow is already running and if so + # wait for the existing run to complete. Note, in many production setups, + # the client would be in a completely separate process from the worker. + try: + result = await client.execute_workflow( + PatchWorkflow.run, + version, + id="hello-patch-workflow-id", + task_queue="hello-patch-task-queue", + ) + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + result = await client.get_workflow_handle( + "hello-patch-workflow-id" + ).result() + print(f"Result: {result}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/hello/patch/README.md b/hello/patch/README.md deleted file mode 100644 index 6d7a9eb2..00000000 --- a/hello/patch/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Patch Workflow Sample -This sample will demonstrate how to version a workflow. The sample workflow will execute a activity and fire a timer. - -- First run the v1 workflow -- Ctrl-C and interrupt workflow -- Run the v2 workflow - - The workflow will is running, execution resumes (after timer fires) and your v1 code is run -- Re-run the v2 workflow - - The workflow will execute your new v2 code diff --git a/hello/patch/hello_patch-v1.py b/hello/patch/hello_patch-v1.py deleted file mode 100644 index 4e0ce2c1..00000000 --- a/hello/patch/hello_patch-v1.py +++ /dev/null @@ -1,77 +0,0 @@ -import asyncio -import logging -from dataclasses import dataclass -from datetime import timedelta - -from temporalio import activity, exceptions, workflow -from temporalio.client import Client -from temporalio.worker import Worker - - -# While we could use multiple parameters in the activity, Temporal strongly -# encourages using a single dataclass instead which can have fields added to it -# in a backwards-compatible way. -@dataclass -class ComposeGreetingInput: - greeting: str - name: str - - -# Basic activity that logs and does string concatenation -@activity.defn -async def compose_greeting(input: ComposeGreetingInput) -> str: - activity.logger.info("Running activity with parameter %s" % input) - return f"{input.greeting}, {input.name}!" - - -@workflow.defn -class PatchedWorkflow: - @workflow.run - async def run(self, name: str) -> str: - workflow.logger.info("Running workflow with parameter %s" % name) - greeting = await workflow.execute_activity( - compose_greeting, - ComposeGreetingInput("Hello", name), - start_to_close_timeout=timedelta(seconds=70), - ) - await asyncio.sleep(60) - return greeting - - -async def main(): - # Uncomment the line below to see logging - # logging.basicConfig(level=logging.INFO) - - # Start client - client = await Client.connect("localhost:7233") - - # Run a worker for the workflow - async with Worker( - client, - task_queue="hello-patched-task-queue", - workflows=[PatchedWorkflow], - activities=[compose_greeting], - ): - - # While the worker is running, use the client to run the workflow and - # print out its result. Check if the workflow is already running and if so - # wait for it to complete. Note, in many production setups, the client - # would be in a completely separate process from the worker. - try: - result = await client.execute_workflow( - PatchedWorkflow.run, - "World", - id="hello-patched-workflow-id", - task_queue="hello-patched-task-queue", - ) - print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: - print(f"Workflow already running") - workflow_result = await client.get_workflow_handle( - "hello-patched-workflow-id" - ).result() - print(f"Successful workflow result: {workflow_result}") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/hello/patch/hello_patch-v2.py b/hello/patch/hello_patch-v2.py deleted file mode 100644 index 69747269..00000000 --- a/hello/patch/hello_patch-v2.py +++ /dev/null @@ -1,89 +0,0 @@ -import asyncio -import logging -from dataclasses import dataclass -from datetime import timedelta - -from temporalio import activity, exceptions, workflow -from temporalio.client import Client -from temporalio.worker import Worker - - -# While we could use multiple parameters in the activity, Temporal strongly -# encourages using a single dataclass instead which can have fields added to it -# in a backwards-compatible way. -@dataclass -class ComposeGreetingInput: - greeting: str - name: str - - -# Basic activity that logs and does string concatenation -@activity.defn -async def compose_greeting(input: ComposeGreetingInput) -> str: - activity.logger.info("Running activity with parameter %s" % input) - return f"{input.greeting}, {input.name}!" - - -@workflow.defn -class PatchedWorkflow: - @workflow.run - async def run(self, name: str) -> str: - if workflow.patched("my-patch-v2"): - print(f"Running new version of workflow") - workflow.logger.info("Running new v1 workflow with parameter %s" % name) - greeting = await workflow.execute_activity( - compose_greeting, - ComposeGreetingInput("Hello-v2", name), - start_to_close_timeout=timedelta(seconds=70), - ) - await asyncio.sleep(60) - return greeting - else: - print(f"Running old version of workflow") - workflow.logger.info("Running original workflow with parameter %s" % name) - greeting = await workflow.execute_activity( - compose_greeting, - ComposeGreetingInput("Hello", name), - start_to_close_timeout=timedelta(seconds=70), - ) - await asyncio.sleep(60) - return greeting - - -async def main(): - # Uncomment the line below to see logging - # logging.basicConfig(level=logging.INFO) - - # Start client - client = await Client.connect("localhost:7233") - - # Run a worker for the workflow - async with Worker( - client, - task_queue="hello-patched-task-queue", - workflows=[PatchedWorkflow], - activities=[compose_greeting], - ): - - # While the worker is running, use the client to run the workflow and - # print out its result. Check if the workflow is already running and if so - # wait for it to complete. Note, in many production setups, the client - # would be in a completely separate process from the worker. - try: - result = await client.execute_workflow( - PatchedWorkflow.run, - "World", - id="hello-patched-workflow-id", - task_queue="hello-patched-task-queue", - ) - print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: - print(f"Workflow already running") - workflow_result = await client.get_workflow_handle( - "hello-patched-workflow-id" - ).result() - print(f"Successful workflow result: {workflow_result}") - - -if __name__ == "__main__": - asyncio.run(main()) From cdec801e4dfdb65503e0d1bee0f902f134c22352 Mon Sep 17 00:00:00 2001 From: ktenzer Date: Tue, 28 Feb 2023 15:40:37 -0800 Subject: [PATCH 08/10] separated versions through workflow definitions and added depricate to example Signed-off-by: ktenzer --- hello/hello_patch.py | 180 ++++++++++++++++++++++++++++++------------- 1 file changed, 126 insertions(+), 54 deletions(-) diff --git a/hello/hello_patch.py b/hello/hello_patch.py index 96c20c41..61e6ff18 100644 --- a/hello/hello_patch.py +++ b/hello/hello_patch.py @@ -14,7 +14,7 @@ # in a backwards-compatible way. @dataclass class ComposeGreetingInput: - greeting = "Hello" + greeting: str name: str @@ -25,60 +25,90 @@ async def compose_greeting(input: ComposeGreetingInput) -> str: return f"{input.greeting}, {input.name}!" -# Depending on version argument passed in, will execute a different workflow. -# The v1 workflow shows the original workflow with a single activity that -# outputs "Hello, World". If we wanted to change this workflow without breaking -# already running workflows, we can use patched. The v2 workflow shows how to -# use patched to continue to output "Hello, World" for already running workflows -# and output "Hello, Universe" for newly started workflows. The timer (sleep) exists -# to allow experimentation, around how changes affect running workflows. +# MyWorkflowDeployedFirst workflow is example of first version of workflow @workflow.defn -class PatchWorkflow: +class MyWorkflowDeployedFirst: @workflow.run - async def run(self, version: str) -> str: - greeting = "" - if version == "v1": - print(f"Running workflow version {version}") - workflow.logger.info("Running workflow version {version}") + async def run(self, name: str) -> str: + print(f"Running MyWorkflowDeployedFirst workflow with parameter %s" % name) + workflow.logger.info( + "Running MyWorkflowDeployedFirst workflow with parameter %s" % name + ) + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Hello", name), + start_to_close_timeout=timedelta(seconds=70), + ) + return greeting + + +# MyWorkflowPatched workflow is example of using patch to change the workflow for +# newly started workflows without changing the behavior of existing workflows. +@workflow.defn +class MyWorkflowPatched: + @workflow.run + async def run(self, name: str) -> str: + print(f"Running MyWorkflowPatched workflow with parameter %s" % name) + workflow.logger.info( + "Running MyWorkflowPatched workflow with parameter %s" % name + ) + if workflow.patched("my-patch-v2"): greeting = await workflow.execute_activity( compose_greeting, - ComposeGreetingInput("World"), + ComposeGreetingInput("Goodbye", name), start_to_close_timeout=timedelta(seconds=70), ) - await asyncio.sleep(60) - elif version == "v2": - print(f"Running workflow version {version}") - workflow.logger.info("Running workflow version {version}") - if workflow.patched("my-patch-v2"): - greeting = await workflow.execute_activity( - compose_greeting, - ComposeGreetingInput("Universe"), - start_to_close_timeout=timedelta(seconds=70), - ) + print(f"Fire a timer and sleep for 10 seconds") + await asyncio.sleep(10) + return greeting + else: + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Hello", name), + start_to_close_timeout=timedelta(seconds=70), + ) + return greeting - await asyncio.sleep(60) - else: - greeting = await workflow.execute_activity( - compose_greeting, - ComposeGreetingInput("World"), - start_to_close_timeout=timedelta(seconds=70), - ) - await asyncio.sleep(60) +# MyWorkflowPatchDeprecated workflow is example for after no older workflows are +# running and we want to deprecate the patch and just use the new version. +@workflow.defn +class MyWorkflowPatchDeprecated: + @workflow.run + async def run(self, name: str) -> str: + print(f"Running MyWorkflowPatchDeprecated workflow with parameter %s" % name) + workflow.logger.info( + "Running MyWorkflowPatchDeprecated workflow with parameter %s" % name + ) + workflow.deprecate_patch("my-patch-v2") + greeting = await workflow.execute_activity( + compose_greeting, + ComposeGreetingInput("Goodbye", name), + start_to_close_timeout=timedelta(seconds=70), + ) + + print(f"Fire a timer and sleep for 10 seconds") + await asyncio.sleep(10) return greeting async def main(): - # Check arguments and ensure either v1 or v2 is passed in + # Check arguments and ensure either v1, v2 or v3 is passed in as argument if len(sys.argv) > 2: - print(f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2") + print( + f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" + ) exit() if len(sys.argv) <= 1: - print(f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2") + print( + f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" + ) exit() - if sys.argv[1] != "v1" and sys.argv[1] != "v2": - print(f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2") + if sys.argv[1] != "v1" and sys.argv[1] != "v2" and sys.argv[1] != "v3": + print( + f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" + ) exit() version = sys.argv[1] @@ -93,27 +123,69 @@ async def main(): async with Worker( client, task_queue="hello-patch-task-queue", - workflows=[PatchWorkflow], + workflows=[ + MyWorkflowDeployedFirst, + MyWorkflowPatched, + MyWorkflowPatchDeprecated, + ], activities=[compose_greeting], ): # While the worker is running, use the client to run the workflow and - # print out its result. Check if the workflow is already running and if so - # wait for the existing run to complete. Note, in many production setups, + # print out its result. A workflow will be chosen based on argument passed in. + # Check if the workflow is already running and if so wait for the + # existing run to complete. Note, in many production setups, # the client would be in a completely separate process from the worker. - try: - result = await client.execute_workflow( - PatchWorkflow.run, - version, - id="hello-patch-workflow-id", - task_queue="hello-patch-task-queue", + + if version == "v1": + try: + result = await client.execute_workflow( + MyWorkflowDeployedFirst.run, + "World", + id="hello-patch-workflow-id", + task_queue="hello-patch-task-queue", + ) + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + result = await client.get_workflow_handle( + "hello-patch-workflow-id" + ).result() + print(f"Result: {result}") + elif version == "v2": + try: + result = await client.execute_workflow( + MyWorkflowPatched.run, + "World", + id="hello-patch-workflow-id", + task_queue="hello-patch-task-queue", + ) + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + result = await client.get_workflow_handle( + "hello-patch-workflow-id" + ).result() + print(f"Result: {result}") + elif version == "v3": + try: + result = await client.execute_workflow( + MyWorkflowPatchDeprecated.run, + "World", + id="hello-patch-workflow-id", + task_queue="hello-patch-task-queue", + ) + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + result = await client.get_workflow_handle( + "hello-patch-workflow-id" + ).result() + print(f"Result: {result}") + else: + print( + f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" ) - print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: - print(f"Workflow already running") - result = await client.get_workflow_handle( - "hello-patch-workflow-id" - ).result() - print(f"Result: {result}") + exit() if __name__ == "__main__": From 9bd6d89d2a7d90ecf5a9b3ce2c8da94cee4f6692 Mon Sep 17 00:00:00 2001 From: ktenzer Date: Tue, 28 Feb 2023 19:36:41 -0800 Subject: [PATCH 09/10] re-factored to utilize same workflow and make example of patching easier to follow Signed-off-by: ktenzer --- hello/hello_patch.py | 141 +++++++++++++++---------------------------- 1 file changed, 49 insertions(+), 92 deletions(-) diff --git a/hello/hello_patch.py b/hello/hello_patch.py index 61e6ff18..19d19566 100644 --- a/hello/hello_patch.py +++ b/hello/hello_patch.py @@ -25,15 +25,13 @@ async def compose_greeting(input: ComposeGreetingInput) -> str: return f"{input.greeting}, {input.name}!" -# MyWorkflowDeployedFirst workflow is example of first version of workflow -@workflow.defn -class MyWorkflowDeployedFirst: +# V1 of patch-workflow +@workflow.defn(name="patch-workflow") +class MyWorkflow: @workflow.run async def run(self, name: str) -> str: - print(f"Running MyWorkflowDeployedFirst workflow with parameter %s" % name) - workflow.logger.info( - "Running MyWorkflowDeployedFirst workflow with parameter %s" % name - ) + print(f"Running patch-workflow with parameter %s" % name) + workflow.logger.info("Running patch-workflow with parameter %s" % name) greeting = await workflow.execute_activity( compose_greeting, ComposeGreetingInput("Hello", name), @@ -42,16 +40,14 @@ async def run(self, name: str) -> str: return greeting -# MyWorkflowPatched workflow is example of using patch to change the workflow for -# newly started workflows without changing the behavior of existing workflows. -@workflow.defn +# V2 of patch-workflow using patched where we have changed newly started +# workflow behavior without changing the behavior of currently running workflows +@workflow.defn(name="patch-workflow") class MyWorkflowPatched: @workflow.run async def run(self, name: str) -> str: - print(f"Running MyWorkflowPatched workflow with parameter %s" % name) - workflow.logger.info( - "Running MyWorkflowPatched workflow with parameter %s" % name - ) + print(f"Running patch-workflow with parameter %s" % name) + workflow.logger.info("Running patch-workflow with parameter %s" % name) if workflow.patched("my-patch-v2"): greeting = await workflow.execute_activity( compose_greeting, @@ -71,16 +67,14 @@ async def run(self, name: str) -> str: return greeting -# MyWorkflowPatchDeprecated workflow is example for after no older workflows are -# running and we want to deprecate the patch and just use the new version. -@workflow.defn +# V3 of patch-workflow using deprecate_patch where all the old V1 workflows +# have completed, we no longer need to preserve V1 and now just have V2 +@workflow.defn(name="patch-worklow") class MyWorkflowPatchDeprecated: @workflow.run async def run(self, name: str) -> str: - print(f"Running MyWorkflowPatchDeprecated workflow with parameter %s" % name) - workflow.logger.info( - "Running MyWorkflowPatchDeprecated workflow with parameter %s" % name - ) + print(f"Running patch-workflow with parameter %s" % name) + workflow.logger.info("Running patch-workflow with parameter %s" % name) workflow.deprecate_patch("my-patch-v2") greeting = await workflow.execute_activity( compose_greeting, @@ -94,21 +88,15 @@ async def run(self, name: str) -> str: async def main(): - # Check arguments and ensure either v1, v2 or v3 is passed in as argument + # Check Args if len(sys.argv) > 2: - print( - f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" - ) + print(f"Incorrect arguments: {sys.argv[0]} v1|v2|v3") exit() if len(sys.argv) <= 1: - print( - f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" - ) + print(f"Incorrect arguments: {sys.argv[0]} v1|v2|v3v3") exit() if sys.argv[1] != "v1" and sys.argv[1] != "v2" and sys.argv[1] != "v3": - print( - f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" - ) + print(f"Incorrect arguments: {sys.argv[0]} v1|v2|v3") exit() version = sys.argv[1] @@ -119,73 +107,42 @@ async def main(): # Start client client = await Client.connect("localhost:7233") - # Run a worker for the workflow + # Set workflow_class to the proper class based on version + workflow_class = "" + if version == "v1": + workflow_class = MyWorkflow # type: ignore + elif version == "v2": + workflow_class = MyWorkflowPatched # type: ignore + elif version == "v3": + workflow_class = MyWorkflowPatchDeprecated # type: ignore + else: + print(f"Incorrect arguments: {sys.argv[0]} v1|v2|v3") + exit() + + # While the worker is running, use the client to run the workflow and + # print out its result. Check if the workflow is already running and + # if so wait for the existing run to complete. Note, in many production setups, + # the client would be in a completely separate process from the worker. async with Worker( client, task_queue="hello-patch-task-queue", - workflows=[ - MyWorkflowDeployedFirst, - MyWorkflowPatched, - MyWorkflowPatchDeprecated, - ], + workflows=[workflow_class], # type: ignore activities=[compose_greeting], ): - # While the worker is running, use the client to run the workflow and - # print out its result. A workflow will be chosen based on argument passed in. - # Check if the workflow is already running and if so wait for the - # existing run to complete. Note, in many production setups, - # the client would be in a completely separate process from the worker. - - if version == "v1": - try: - result = await client.execute_workflow( - MyWorkflowDeployedFirst.run, - "World", - id="hello-patch-workflow-id", - task_queue="hello-patch-task-queue", - ) - print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: - print(f"Workflow already running") - result = await client.get_workflow_handle( - "hello-patch-workflow-id" - ).result() - print(f"Result: {result}") - elif version == "v2": - try: - result = await client.execute_workflow( - MyWorkflowPatched.run, - "World", - id="hello-patch-workflow-id", - task_queue="hello-patch-task-queue", - ) - print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: - print(f"Workflow already running") - result = await client.get_workflow_handle( - "hello-patch-workflow-id" - ).result() - print(f"Result: {result}") - elif version == "v3": - try: - result = await client.execute_workflow( - MyWorkflowPatchDeprecated.run, - "World", - id="hello-patch-workflow-id", - task_queue="hello-patch-task-queue", - ) - print(f"Result: {result}") - except exceptions.WorkflowAlreadyStartedError: - print(f"Workflow already running") - result = await client.get_workflow_handle( - "hello-patch-workflow-id" - ).result() - print(f"Result: {result}") - else: - print( - f"Incorrect arguments: {sys.argv[0]} v1 or {sys.argv[0]} v2 or {sys.argv[0]} v3" + try: + result = await client.execute_workflow( + workflow_class.run, # type: ignore + "World", + id="hello-patch-workflow-id", + task_queue="hello-patch-task-queue", ) - exit() + print(f"Result: {result}") + except exceptions.WorkflowAlreadyStartedError: + print(f"Workflow already running") + result = await client.get_workflow_handle( + "hello-patch-workflow-id" + ).result() + print(f"Result: {result}") if __name__ == "__main__": From 8807ccf36b81bdba370976419c8c55341aaf6faf Mon Sep 17 00:00:00 2001 From: ktenzer Date: Wed, 1 Mar 2023 11:38:40 -0800 Subject: [PATCH 10/10] removed print statements from workflow Signed-off-by: ktenzer --- hello/hello_patch.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/hello/hello_patch.py b/hello/hello_patch.py index 19d19566..43371d4f 100644 --- a/hello/hello_patch.py +++ b/hello/hello_patch.py @@ -30,7 +30,6 @@ async def compose_greeting(input: ComposeGreetingInput) -> str: class MyWorkflow: @workflow.run async def run(self, name: str) -> str: - print(f"Running patch-workflow with parameter %s" % name) workflow.logger.info("Running patch-workflow with parameter %s" % name) greeting = await workflow.execute_activity( compose_greeting, @@ -46,7 +45,6 @@ async def run(self, name: str) -> str: class MyWorkflowPatched: @workflow.run async def run(self, name: str) -> str: - print(f"Running patch-workflow with parameter %s" % name) workflow.logger.info("Running patch-workflow with parameter %s" % name) if workflow.patched("my-patch-v2"): greeting = await workflow.execute_activity( @@ -55,7 +53,6 @@ async def run(self, name: str) -> str: start_to_close_timeout=timedelta(seconds=70), ) - print(f"Fire a timer and sleep for 10 seconds") await asyncio.sleep(10) return greeting else: @@ -73,7 +70,6 @@ async def run(self, name: str) -> str: class MyWorkflowPatchDeprecated: @workflow.run async def run(self, name: str) -> str: - print(f"Running patch-workflow with parameter %s" % name) workflow.logger.info("Running patch-workflow with parameter %s" % name) workflow.deprecate_patch("my-patch-v2") greeting = await workflow.execute_activity( @@ -82,7 +78,6 @@ async def run(self, name: str) -> str: start_to_close_timeout=timedelta(seconds=70), ) - print(f"Fire a timer and sleep for 10 seconds") await asyncio.sleep(10) return greeting