From ad3cfdea462d045616a6283a19ed1bb2f8672ae1 Mon Sep 17 00:00:00 2001 From: GSmithApps Date: Tue, 13 May 2025 16:12:19 -0500 Subject: [PATCH 1/9] EDU-4430 python sync vs async --- .../python/python-sdk-sync-vs-async.mdx | 160 +++++++++++++----- 1 file changed, 113 insertions(+), 47 deletions(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index 080001376f..bf8d9b06d8 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -67,65 +67,50 @@ asynchronous Activity would lead to blocking your event loop. If you want to mak an HTTP call from within an asynchronous Activity, you should use an async-safe HTTP library such as `aiohttp` or `httpx`. Otherwise, use a synchronous Activity. -## Implementing Asynchronous Activities -The following code is an asynchronous Activity Definition that's similar to one -you will use during an upcoming exercise. Like the Workflow Definition -you've already run, it takes a name (`str`) as input and returns a -customized greeting (`str`) as output. However, this Activity makes -a call to a microservice, accessed through HTTP, to request this -greeting in Spanish. This activity uses the `aiohttp` library to make an async -safe HTTP request. Using the `requests` library here would have resulting in -blocking code within the async event loop, which will block the entire async -event loop. For more in-depth information about this issue, refer to the -[Python asyncio documentation](https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code). +## Python SDK Worker Execution Architecture -The code below also implements the Activity Definition as a class, rather than a -function. The `aiohttp` library requires an established `Session` to perform the -HTTP request. It would be inefficient to establish a `Session` every time an -Activity is invoked, so instead this code accepts a `Session` object as an instance -parameter and makes it available to the methods. This approach will also be -beneficial when the execution is over and the `Session` needs to be closed. +Python workers have following components for executing code: -In this example, the Activity supplies the name in the URL and retrieves -the greeting from the body of the response. +- Your event loop, which runs tasks from async activities **plus the rest of Temporal, such as communicating with the server**. +- An executor for executing activity tasks from synchronous activities. A thread pool executor is recommended. +- A thread pool executor for executing workflow tasks (see forum post [here](https://community.temporal.io/t/whats-the-workflow-task-executor-in-the-python-worker-configuration/16965)). -```python -import aiohttp -import urllib.parse -from temporalio import activity +> See Also: [docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()` -class TranslateActivities: - def __init__(self, session: aiohttp.ClientSession): - self.session = session - @activity.defn - async def greet_in_spanish(self, name: str) -> str: - greeting = await self.call_service("get-spanish-greeting", name) - return greeting +## Activity Definition - # Utility method for making calls to the microservices - async def call_service(self, stem: str, name: str) -> str: - base = f"http://localhost:9999/{stem}" - url = f"{base}?name={urllib.parse.quote(name)}" +**By default, activities should be synchronous rather than asynchronous**. +You should only make an activity asynchronous if you are *very* +sure that it does not block the event loop. - async with self.session.get(url) as response: - translation = await response.text() +This is because if you have blocking code in an `async def` function, +it blocks your event loop and the rest of Temporal, which can cause bugs that are +very hard to diagnose, including freezing your worker and blocking workflow progress +(because Temporal can't tell the server that workflow tasks are completing). +The reason synchronous activities help is because they +run in the `activity_executor` ([docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()`) +rather than in the global event loop, +which helps because: - if response.status >= 400: - raise ApplicationError( - f"HTTP Error {response.status}: {translation}", - # We want to have Temporal automatically retry 5xx but not 4xx - non_retryable=response.status < 500, - ) +* There's no risk of accidentally blocking the global event loop. +* If you have multiple + activity tasks running in a thread pool rather than an event loop, one bad + activity task can't slow down the others; this is because the OS scheduler preemptively + switches between threads, which the event loop coordinator does not do. - return translation -``` +> See Also: +> ["Types of Activities" section of Python SDK README](https://github.com/temporalio/sdk-python#types-of-activities) +> and [Sync vs Async activity implementations](https://docs.temporal.io/develop/python/python-sdk-sync-vs-async) ## Implementing Synchronous Activities -The following code is an implementation of the above Activity, but as a -synchronous Activity Definition. When making the call to the microservice, +The following code is a synchronous Activity Definition. +It takes a name (`str`) as input and returns a +customized greeting (`str`) as output. + +It makes a call to a microservice, and when making this call, you'll notice that it uses the `requests` library. This is safe to do in synchronous Activities. @@ -153,7 +138,7 @@ class TranslateActivities: In the above example we chose not to share a session across the Activity, so `__init__` was removed. While `requests` does have the ability to create sessions, it is currently unknown if they are thread safe. Due to no longer having or needing -`__ini__`, the case could be made here to not implement the Activities as a class, +`__init__`, the case could be made here to not implement the Activities as a class, but just as decorated functions as shown below: ```python @@ -175,6 +160,86 @@ Whether to implement Activities as class methods or functions is a design choice choice left up to the developer when cross-activity state is not needed. Both are equally valid implementations. +### Worker for Synchronous Activities + +When running synchronous activities, the Worker +neets to have an `activity_executor`. Temporal +recommends using a `ThreadPoolExecutor` as shown here: + +```python +with ThreadPoolExecutor(max_workers=42) as executor: + worker = Worker( + # ... + activity_executor=executor, + # ... + ) +``` + +## Implementing Asynchronous Activities + +The following code is an implementation of the above Activity, but as an +asynchronous Activity Definition. + +It makes +a call to a microservice, accessed through HTTP, to request this +greeting in Spanish. This activity uses the `aiohttp` library to make an async +safe HTTP request. Using the `requests` library here would have resulting in +blocking code within the async event loop, which will block the entire async +event loop. For more in-depth information about this issue, refer to the +[Python asyncio documentation](https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code). + +The code below also implements the Activity Definition as a class, rather than a +function. The `aiohttp` library requires an established `Session` to perform the +HTTP request. It would be inefficient to establish a `Session` every time an +Activity is invoked, so instead this code accepts a `Session` object as an instance +parameter and makes it available to the methods. This approach will also be +beneficial when the execution is over and the `Session` needs to be closed. + +In this example, the Activity supplies the name in the URL and retrieves +the greeting from the body of the response. + +```python +import aiohttp +import urllib.parse +from temporalio import activity + +class TranslateActivities: + def __init__(self, session: aiohttp.ClientSession): + self.session = session + + @activity.defn + async def greet_in_spanish(self, name: str) -> str: + greeting = await self.call_service("get-spanish-greeting", name) + return greeting + + # Utility method for making calls to the microservices + async def call_service(self, stem: str, name: str) -> str: + base = f"http://localhost:9999/{stem}" + url = f"{base}?name={urllib.parse.quote(name)}" + + async with self.session.get(url) as response: + translation = await response.text() + + if response.status >= 400: + raise ApplicationError( + f"HTTP Error {response.status}: {translation}", + # We want to have Temporal automatically retry 5xx but not 4xx + non_retryable=response.status < 500, + ) + + return translation +``` + +### Running synchronous code from an asynchronous function + +If your activity is asynchronous and you don't want to change it to synchronous, +but you need to run blocking code inside it, +then you can use python utility functions to run synchronous code +in an asynchronous function: + +- [`loop.run_in_executor()`](https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor), which is also mentioned in the ["running blocking code" section of the "developing with asyncio" guide](https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code) +- [`asyncio.to_thread()`](https://docs.python.org/3/library/asyncio-task.html#running-in-threads) + ## When Should You Use Async Activities Asynchronous Activities have many advantages, such as potential speed up of execution. @@ -184,3 +249,4 @@ using asynchronous Activities _only_ when you are certain that your Activities are async safe and don't make blocking calls. If you experience bugs that you think may be a result of an unsafe call being made in an asynchronous Activity, convert it to a synchronous Activity and see if the issue resolves. + From a38290497b74879b539d0b4a78316ec6bd877831 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 13 May 2025 21:13:34 +0000 Subject: [PATCH 2/9] CI: Automatic .md and .mdx formatting --- docs/develop/python/python-sdk-sync-vs-async.mdx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index bf8d9b06d8..3ca2eb9fc8 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -67,7 +67,6 @@ asynchronous Activity would lead to blocking your event loop. If you want to mak an HTTP call from within an asynchronous Activity, you should use an async-safe HTTP library such as `aiohttp` or `httpx`. Otherwise, use a synchronous Activity. - ## Python SDK Worker Execution Architecture Python workers have following components for executing code: @@ -78,11 +77,10 @@ Python workers have following components for executing code: > See Also: [docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()` - ## Activity Definition **By default, activities should be synchronous rather than asynchronous**. -You should only make an activity asynchronous if you are *very* +You should only make an activity asynchronous if you are _very_ sure that it does not block the event loop. This is because if you have blocking code in an `async def` function, @@ -94,8 +92,8 @@ run in the `activity_executor` ([docs for](https://python.temporal.io/temporalio rather than in the global event loop, which helps because: -* There's no risk of accidentally blocking the global event loop. -* If you have multiple +- There's no risk of accidentally blocking the global event loop. +- If you have multiple activity tasks running in a thread pool rather than an event loop, one bad activity task can't slow down the others; this is because the OS scheduler preemptively switches between threads, which the event loop coordinator does not do. @@ -106,7 +104,7 @@ which helps because: ## Implementing Synchronous Activities -The following code is a synchronous Activity Definition. +The following code is a synchronous Activity Definition. It takes a name (`str`) as input and returns a customized greeting (`str`) as output. @@ -163,7 +161,7 @@ equally valid implementations. ### Worker for Synchronous Activities When running synchronous activities, the Worker -neets to have an `activity_executor`. Temporal +neets to have an `activity_executor`. Temporal recommends using a `ThreadPoolExecutor` as shown here: ```python @@ -249,4 +247,3 @@ using asynchronous Activities _only_ when you are certain that your Activities are async safe and don't make blocking calls. If you experience bugs that you think may be a result of an unsafe call being made in an asynchronous Activity, convert it to a synchronous Activity and see if the issue resolves. - From 5238204b56b531add49795c219bef67526e6969c Mon Sep 17 00:00:00 2001 From: Grant Date: Wed, 14 May 2025 12:29:18 -0500 Subject: [PATCH 3/9] Update python-sdk-sync-vs-async.mdx --- docs/develop/python/python-sdk-sync-vs-async.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index 3ca2eb9fc8..2eac6038a0 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -228,7 +228,7 @@ class TranslateActivities: return translation ``` -### Running synchronous code from an asynchronous function +### Running synchronous code from an asynchronous activity If your activity is asynchronous and you don't want to change it to synchronous, but you need to run blocking code inside it, From 2440e59282a978c336468457e4a2ccbbeffb6220 Mon Sep 17 00:00:00 2001 From: GSmithApps Date: Wed, 14 May 2025 13:06:46 -0500 Subject: [PATCH 4/9] EDU-4430 responding to review --- .../python/python-sdk-sync-vs-async.mdx | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index 3ca2eb9fc8..a4cbe2e44a 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -77,6 +77,35 @@ Python workers have following components for executing code: > See Also: [docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()` + + +### Activities + +- The activities and the temporal worker SDK code both run in whatever event loop the user gives the worker (which is often the default asyncio event loop). +- Synchronous Activities run in the `activity_executor`. + +### Workflows + +Since workflow tasks (1) are CPU bound, (2) need to be timed out for deadlock detection, and (3) need to not block other workflow tasks, they are run in threads. The `workflow_task_executor` is the thread pool these tasks are run on. + +This can be confusing at first because Workflow Definitions are `async`, but this `async` is not referring to the standard event loop -- it is referring to the workflow's own SDK event loop. +Each workflow gets its own “workflow event loop”, which is deterministic, and described in [the Python SDK blog](https://temporal.io/blog/durable-distributed-asyncio-event-loop#temporal-workflows-are-asyncio-event-loops). +The workflow event loop doesn't constantly loop – it just gets cycled through during a workflow task to make as much progress as possible on any all of its futures. +When it can no longer make progress on any of its futures, then the Workflow Task is complete. + +### Number of cores + +The only ways to use more than one CPU core in a python worker (considering the GIL) are: + +- Run the sync activities in a process pool executor, but a thread pool executor is recommended. +- Run more than one worker process. + +### Separating Activity and Workflow Workers + +To reduce the risk of event loops or executors getting blocked, +some users choose to deploy separate workers for workflow tasks and activity tasks. + + ## Activity Definition **By default, activities should be synchronous rather than asynchronous**. From 987ad6c81f014c6c4c673c46540dce03e5c8b741 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 14 May 2025 18:08:19 +0000 Subject: [PATCH 5/9] CI: Automatic .md and .mdx formatting --- docs/develop/python/python-sdk-sync-vs-async.mdx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index 8a35d0da2e..289ffc0c7f 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -77,8 +77,6 @@ Python workers have following components for executing code: > See Also: [docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()` - - ### Activities - The activities and the temporal worker SDK code both run in whatever event loop the user gives the worker (which is often the default asyncio event loop). @@ -103,8 +101,7 @@ The only ways to use more than one CPU core in a python worker (considering the ### Separating Activity and Workflow Workers To reduce the risk of event loops or executors getting blocked, -some users choose to deploy separate workers for workflow tasks and activity tasks. - +some users choose to deploy separate workers for workflow tasks and activity tasks. ## Activity Definition From 5f2a4e15ed8ea49722a2ff3aa210ad771062814e Mon Sep 17 00:00:00 2001 From: GSmithApps Date: Wed, 14 May 2025 14:20:39 -0500 Subject: [PATCH 6/9] EDU-4430 cleaned explanation --- .../python/python-sdk-sync-vs-async.mdx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index 289ffc0c7f..d2bc002833 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -71,37 +71,38 @@ such as `aiohttp` or `httpx`. Otherwise, use a synchronous Activity. Python workers have following components for executing code: -- Your event loop, which runs tasks from async activities **plus the rest of Temporal, such as communicating with the server**. +- Your event loop, which runs tasks from async activities **plus the rest of the Temporal Worker, such as communicating with the server**. - An executor for executing activity tasks from synchronous activities. A thread pool executor is recommended. -- A thread pool executor for executing workflow tasks (see forum post [here](https://community.temporal.io/t/whats-the-workflow-task-executor-in-the-python-worker-configuration/16965)). +- A thread pool executor for executing workflow tasks. > See Also: [docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()` ### Activities -- The activities and the temporal worker SDK code both run in whatever event loop the user gives the worker (which is often the default asyncio event loop). +- The activities and the temporal worker SDK code both run the default asyncio event loop or whatever event loop you give the Worker. - Synchronous Activities run in the `activity_executor`. ### Workflows Since workflow tasks (1) are CPU bound, (2) need to be timed out for deadlock detection, and (3) need to not block other workflow tasks, they are run in threads. The `workflow_task_executor` is the thread pool these tasks are run on. -This can be confusing at first because Workflow Definitions are `async`, but this `async` is not referring to the standard event loop -- it is referring to the workflow's own SDK event loop. +The fact that Workflow Tasks run in a thread pool can be confusing at first because Workflow Definitions are `async`. +The key differentiator is that the `async` in Workflow Definitions is not referring to the standard event loop -- it is referring to the workflow's own event loop. Each workflow gets its own “workflow event loop”, which is deterministic, and described in [the Python SDK blog](https://temporal.io/blog/durable-distributed-asyncio-event-loop#temporal-workflows-are-asyncio-event-loops). -The workflow event loop doesn't constantly loop – it just gets cycled through during a workflow task to make as much progress as possible on any all of its futures. +The workflow event loop doesn't constantly loop -- it just gets cycled through during a workflow task to make as much progress as possible on all of its futures. When it can no longer make progress on any of its futures, then the Workflow Task is complete. -### Number of cores +### Number of CPU cores -The only ways to use more than one CPU core in a python worker (considering the GIL) are: +The only ways to use more than one core in a python worker (considering Python's GIL) are: -- Run the sync activities in a process pool executor, but a thread pool executor is recommended. - Run more than one worker process. +- Run the sync activities in a process pool executor, but a thread pool executor is recommended. ### Separating Activity and Workflow Workers To reduce the risk of event loops or executors getting blocked, -some users choose to deploy separate workers for workflow tasks and activity tasks. +some users choose to deploy separate Workers for Workflow Tasks and Activity Tasks. ## Activity Definition From ecbcd9df674a40782c5501763ca33dc505a47002 Mon Sep 17 00:00:00 2001 From: GSmithApps Date: Mon, 19 May 2025 07:53:21 -0500 Subject: [PATCH 7/9] EDU-4430 fixing lints --- .../python/python-sdk-sync-vs-async.mdx | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index d2bc002833..4485da1602 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -56,7 +56,7 @@ into a synchronous program that executes serially, defeating the entire purpose of using `asyncio`. This can also lead to potential deadlock, and unpredictable behavior that causes tasks to be unable to execute. Debugging these issues can be difficult and time consuming, as locating the source of the blocking call might not always -be immediately obvious. +be immediately evident. Due to this, Python developers must be extra careful to not make blocking calls from within an asynchronous Activity, or use an async safe library to perform @@ -71,25 +71,25 @@ such as `aiohttp` or `httpx`. Otherwise, use a synchronous Activity. Python workers have following components for executing code: -- Your event loop, which runs tasks from async activities **plus the rest of the Temporal Worker, such as communicating with the server**. -- An executor for executing activity tasks from synchronous activities. A thread pool executor is recommended. -- A thread pool executor for executing workflow tasks. +- Your event loop, which runs Tasks from async Activities **plus the rest of the Temporal Worker, such as communicating with the server**. +- An executor for executing Activity Tasks from synchronous Activities. A thread pool executor is recommended. +- A thread pool executor for executing Workflow Tasks. > See Also: [docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()` ### Activities -- The activities and the temporal worker SDK code both run the default asyncio event loop or whatever event loop you give the Worker. +- The Activities and the temporal worker SDK code both run the default asyncio event loop or whatever event loop you give the Worker. - Synchronous Activities run in the `activity_executor`. ### Workflows -Since workflow tasks (1) are CPU bound, (2) need to be timed out for deadlock detection, and (3) need to not block other workflow tasks, they are run in threads. The `workflow_task_executor` is the thread pool these tasks are run on. +Since Workflow Tasks (1) are CPU bound, (2) need to be timed out for deadlock detection, and (3) need to not block other Workflow Tasks, they are run in threads. The `workflow_task_executor` is the thread pool these Tasks are run on. The fact that Workflow Tasks run in a thread pool can be confusing at first because Workflow Definitions are `async`. -The key differentiator is that the `async` in Workflow Definitions is not referring to the standard event loop -- it is referring to the workflow's own event loop. -Each workflow gets its own “workflow event loop”, which is deterministic, and described in [the Python SDK blog](https://temporal.io/blog/durable-distributed-asyncio-event-loop#temporal-workflows-are-asyncio-event-loops). -The workflow event loop doesn't constantly loop -- it just gets cycled through during a workflow task to make as much progress as possible on all of its futures. +The key differentiator is that the `async` in Workflow Definitions isn't referring to the standard event loop -- it's referring to the Workflow's own event loop. +Each Workflow gets its own “Workflow event loop,” which is deterministic, and described in [the Python SDK blog](https://temporal.io/blog/durable-distributed-asyncio-event-loop#temporal-workflows-are-asyncio-event-loops). +The Workflow event loop doesn't constantly loop -- it just gets cycled through during a Workflow Task to make as much progress as possible on all of its futures. When it can no longer make progress on any of its futures, then the Workflow Task is complete. ### Number of CPU cores @@ -97,7 +97,7 @@ When it can no longer make progress on any of its futures, then the Workflow Tas The only ways to use more than one core in a python worker (considering Python's GIL) are: - Run more than one worker process. -- Run the sync activities in a process pool executor, but a thread pool executor is recommended. +- Run the synchronous Activities in a process pool executor, but a thread pool executor is recommended. ### Separating Activity and Workflow Workers @@ -106,30 +106,30 @@ some users choose to deploy separate Workers for Workflow Tasks and Activity Tas ## Activity Definition -**By default, activities should be synchronous rather than asynchronous**. -You should only make an activity asynchronous if you are _very_ +**By default, Activities should be synchronous rather than asynchronous**. +You should only make an Activity asynchronous if you are _very_ sure that it does not block the event loop. This is because if you have blocking code in an `async def` function, it blocks your event loop and the rest of Temporal, which can cause bugs that are -very hard to diagnose, including freezing your worker and blocking workflow progress -(because Temporal can't tell the server that workflow tasks are completing). -The reason synchronous activities help is because they +hard to diagnose, including freezing your worker and blocking Workflow progress +(because Temporal can't tell the server that Workflow Tasks are completing). +The reason synchronous Activities help is because they run in the `activity_executor` ([docs for](https://python.temporal.io/temporalio.worker.Worker.html#__init__) `worker.__init__()`) rather than in the global event loop, which helps because: - There's no risk of accidentally blocking the global event loop. - If you have multiple - activity tasks running in a thread pool rather than an event loop, one bad - activity task can't slow down the others; this is because the OS scheduler preemptively + Activity Tasks running in a thread pool rather than an event loop, one bad + Activity Task can't slow down the others; this is because the OS scheduler preemptively switches between threads, which the event loop coordinator does not do. > See Also: > ["Types of Activities" section of Python SDK README](https://github.com/temporalio/sdk-python#types-of-activities) > and [Sync vs Async activity implementations](https://docs.temporal.io/develop/python/python-sdk-sync-vs-async) -## Implementing Synchronous Activities +## How to implement Synchronous Activities The following code is a synchronous Activity Definition. It takes a name (`str`) as input and returns a @@ -160,11 +160,11 @@ class TranslateActivities: return response.text ``` -In the above example we chose not to share a session across the Activity, so +The preceeding example doesn't share a session across the Activity, so `__init__` was removed. While `requests` does have the ability to create sessions, -it is currently unknown if they are thread safe. Due to no longer having or needing +it's currently unknown if they are thread safe. Due to no longer having or needing `__init__`, the case could be made here to not implement the Activities as a class, -but just as decorated functions as shown below: +but just as decorated functions as shown here: ```python @activity.defn @@ -181,14 +181,14 @@ def call_service(stem: str, name: str) -> str: return response.text ``` -Whether to implement Activities as class methods or functions is a design choice -choice left up to the developer when cross-activity state is not needed. Both are +Whether to implement Activities as class methods or functions is a design +choice left up to the developer when cross-activity state isn't needed. Both are equally valid implementations. -### Worker for Synchronous Activities +### How to run Synchronous Activities on a Worker -When running synchronous activities, the Worker -neets to have an `activity_executor`. Temporal +When running synchronous Activities, the Worker +needs to have an `activity_executor`. Temporal recommends using a `ThreadPoolExecutor` as shown here: ```python @@ -200,20 +200,20 @@ with ThreadPoolExecutor(max_workers=42) as executor: ) ``` -## Implementing Asynchronous Activities +## How to Implement Asynchronous Activities -The following code is an implementation of the above Activity, but as an +The following code is an implementation of the preceeding Activity, but as an asynchronous Activity Definition. It makes a call to a microservice, accessed through HTTP, to request this -greeting in Spanish. This activity uses the `aiohttp` library to make an async +greeting in Spanish. This Activity uses the `aiohttp` library to make an async safe HTTP request. Using the `requests` library here would have resulting in blocking code within the async event loop, which will block the entire async event loop. For more in-depth information about this issue, refer to the [Python asyncio documentation](https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code). -The code below also implements the Activity Definition as a class, rather than a +The following code also implements the Activity Definition as a class, rather than a function. The `aiohttp` library requires an established `Session` to perform the HTTP request. It would be inefficient to establish a `Session` every time an Activity is invoked, so instead this code accepts a `Session` object as an instance @@ -255,9 +255,9 @@ class TranslateActivities: return translation ``` -### Running synchronous code from an asynchronous activity +### How to run synchronous code from an asynchronous activity -If your activity is asynchronous and you don't want to change it to synchronous, +If your Activity is asynchronous and you don't want to change it to synchronous, but you need to run blocking code inside it, then you can use python utility functions to run synchronous code in an asynchronous function: From 7ef260861a1eb1f7fa2fc60bc47a37a912e54b68 Mon Sep 17 00:00:00 2001 From: GSmithApps Date: Tue, 20 May 2025 17:46:52 -0500 Subject: [PATCH 8/9] fixed most lints --- .../python/python-sdk-sync-vs-async.mdx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index 4485da1602..1ae66569a5 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -84,8 +84,13 @@ Python workers have following components for executing code: ### Workflows -Since Workflow Tasks (1) are CPU bound, (2) need to be timed out for deadlock detection, and (3) need to not block other Workflow Tasks, they are run in threads. The `workflow_task_executor` is the thread pool these Tasks are run on. +Since Workflow Tasks have the following three properties, they're run in threads. +* are CPU bound +* need to be timed out for deadlock detection +* need to not block other Workflow Tasks + +The `workflow_task_executor` is the thread pool these Tasks are run on. The fact that Workflow Tasks run in a thread pool can be confusing at first because Workflow Definitions are `async`. The key differentiator is that the `async` in Workflow Definitions isn't referring to the standard event loop -- it's referring to the Workflow's own event loop. Each Workflow gets its own “Workflow event loop,” which is deterministic, and described in [the Python SDK blog](https://temporal.io/blog/durable-distributed-asyncio-event-loop#temporal-workflows-are-asyncio-event-loops). @@ -94,12 +99,12 @@ When it can no longer make progress on any of its futures, then the Workflow Tas ### Number of CPU cores -The only ways to use more than one core in a python worker (considering Python's GIL) are: +The only ways to use more than one core in a python Worker (considering Python's GIL) are: -- Run more than one worker process. +- Run more than one Worker Process. - Run the synchronous Activities in a process pool executor, but a thread pool executor is recommended. -### Separating Activity and Workflow Workers +### A Worker infrastructure option: Separate Activity and Workflow Workers To reduce the risk of event loops or executors getting blocked, some users choose to deploy separate Workers for Workflow Tasks and Activity Tasks. @@ -107,8 +112,8 @@ some users choose to deploy separate Workers for Workflow Tasks and Activity Tas ## Activity Definition **By default, Activities should be synchronous rather than asynchronous**. -You should only make an Activity asynchronous if you are _very_ -sure that it does not block the event loop. +You should only make an Activity asynchronous if you are +certain that it doesn't block the event loop. This is because if you have blocking code in an `async def` function, it blocks your event loop and the rest of Temporal, which can cause bugs that are @@ -123,11 +128,10 @@ which helps because: - If you have multiple Activity Tasks running in a thread pool rather than an event loop, one bad Activity Task can't slow down the others; this is because the OS scheduler preemptively - switches between threads, which the event loop coordinator does not do. + switches between threads, which the event loop coordinator doesn't do. > See Also: > ["Types of Activities" section of Python SDK README](https://github.com/temporalio/sdk-python#types-of-activities) -> and [Sync vs Async activity implementations](https://docs.temporal.io/develop/python/python-sdk-sync-vs-async) ## How to implement Synchronous Activities @@ -162,7 +166,7 @@ class TranslateActivities: The preceeding example doesn't share a session across the Activity, so `__init__` was removed. While `requests` does have the ability to create sessions, -it's currently unknown if they are thread safe. Due to no longer having or needing +it's currently unknown if they're thread safe. Due to no longer having or needing `__init__`, the case could be made here to not implement the Activities as a class, but just as decorated functions as shown here: From 3f55962a6d05dc23b7ed5d888a20573679b98ca5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 20 May 2025 22:47:45 +0000 Subject: [PATCH 9/9] CI: Automatic .md and .mdx formatting --- docs/develop/python/python-sdk-sync-vs-async.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/develop/python/python-sdk-sync-vs-async.mdx b/docs/develop/python/python-sdk-sync-vs-async.mdx index 1ae66569a5..1c1880a334 100644 --- a/docs/develop/python/python-sdk-sync-vs-async.mdx +++ b/docs/develop/python/python-sdk-sync-vs-async.mdx @@ -86,9 +86,9 @@ Python workers have following components for executing code: Since Workflow Tasks have the following three properties, they're run in threads. -* are CPU bound -* need to be timed out for deadlock detection -* need to not block other Workflow Tasks +- are CPU bound +- need to be timed out for deadlock detection +- need to not block other Workflow Tasks The `workflow_task_executor` is the thread pool these Tasks are run on. The fact that Workflow Tasks run in a thread pool can be confusing at first because Workflow Definitions are `async`.