Files
Donald Pinckney b5719bc143 PR Tracking Initial Release (#4)
* Add initial skill for testing, which is simply Steve's skill (#1)

* Add initial skill for testing, which is simply Steve's skill

* Rename skill to 'temporal-dev' and update version

Updated skill name and version for Temporal Python.

* Use claude to merge Steve's, Max's, and Mason's skills.  (#2)

* Use claude to merge Steve's, Max's, and Mason's skills. Did a review pass using claude's skill devlopment skills

* Add missing things from Steve

* trigger tweaks

* Add in common gotchas from Johann

* add simple feedback mechanism (#3)

* Change skill name to kebab-case, for compatibility with Amp and Cline (#7)

* Clean up references/core/ai-integration.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/common-gotchas.md

* Clean up references/core/determinism.md

* Clean up references/core/determinism.md

* Update error-reference.md

* Update interactive-workflows.md

* Clean up patterns.md

* Cut shell scripts

* Edit troubleshooting.md

* remove interceptors for now

* remove dynamic workflows

* clarify on heartbeating of async activity completions, and prompt it a bit in relation to signals

* Improve references/python/advanced-features.md

* Use explicit namespace in connect

* remove duplicated content from determinism.md, clean up

* Improve references/python/data-handling.md

* Prefer start_to_close_timeout

* don't explicitely provide defaults for retry policies

* error-handling.md cleanup

* move idempotency patterns to patterns.md

* remove multi-param activities

* small edits

* Unify sandbox stuff into one file

* local activities aren't experimental

* Clean up references/python/sync-vs-async.md

* Cleanup observability.md, remove duplicated search attributes

* Cut otel for now

* cut a lot of duplicate stuff from python gotchas, address comments

* de-duplicate content

* Lots of improvements to testing

* cleanup to top level of skill (like CLI install instructions), and to top-level of python

* Improve patterns.md

* clean up ai-patterns.md

* Update readme with installation instructions

* remove ts directory

* De-couple core from python and TypeScript as much as possible

* Remove TypeScript hints

* add prompting for feedback at startup - wait for ethan on slack channel

* shorten url

* Update slack channel

* Automated pass over on python cleanup & deduplication

* Remove multi-patching from Python, since its obvious, dont waste tokens on it. (#34)

* Add TypeScript (#31)

Adds initial support for TypeScript to the skill

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>

* Fix typos and reference links (#36)

* Fix typos and reference links

* 2 more typo fixes

* quick edit to readme (#37)

* Fix saga compensations to run under cancellation protection (#43)

When a workflow is cancelled mid-saga, compensations must run in a
cancellation-protected scope, otherwise they are immediately cancelled
before they can execute.

- Python: wrap compensation loop in asyncio.shield() so it runs even
  when the workflow receives a CancelledError
- TypeScript: wrap compensation loop in CancellationScope.nonCancellable()
  so it runs even when the root scope is cancelled (per official docs:
  "Cleanup logic must be in a nonCancellable scope")
- TypeScript: also fix compensation registration order — register BEFORE
  calling the activity (was already correct in Python)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Update readme for public preview (#45)

* a few more readme tweaks (#46)

* Add MIT License to the project (#47)

* Add Go (supersedes other PR) (#38)

* progress on go

* Go translation workflow completed.

* missed a few spots

* Manual edits

* Address feedback

* Add gotcha about anonymous local activities

* Sample code for payload converter

* clarify sdk protection mechanisms

* Setup CODEOWNERS to AI SDK team (#48)

* Align version number in SKILL.md and plugin.json. (#49)

---------

Co-authored-by: James Watkins-Harvey <mjameswh@users.noreply.github.com>
Co-authored-by: Chris Olszewski <chrisdolszewski@gmail.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 17:36:15 -04:00

232 lines
7.1 KiB
Markdown

# Python SDK: Sync vs Async Activities
## Overview
The Temporal Python SDK supports multiple ways of implementing Activities:
- **Asynchronous** using `asyncio`
- **Synchronous multithreaded** using `concurrent.futures.ThreadPoolExecutor`
- **Synchronous multiprocess** using `concurrent.futures.ProcessPoolExecutor`
Choosing the correct approach is critical—incorrect usage can cause sporadic failures and difficult-to-diagnose bugs.
## Recommendation: Default to Synchronous
Activities should be synchronous by default. Use async only when certain the code doesn't block the event loop.
## The Event Loop Problem
The Python async event loop runs in a single thread. When any task runs, no other tasks can execute until an `await` is reached. If code makes a blocking call (file I/O, synchronous HTTP, etc.), the entire event loop freezes.
**Consequences of blocking the event loop:**
- Worker cannot communicate with Temporal Server
- Workflow progress blocks across the worker
- Potential deadlocks and unpredictable behavior
- Difficult-to-diagnose bugs
## How the SDK Handles Each Type
### Synchronous Activities
- Run in the `activity_executor`, which you must provide
- Protected from accidentally blocking the global event loop
- Multiple activities run in parallel via OS thread scheduling
- Thread pool provides preemptive switching between tasks
```python
from concurrent.futures import ThreadPoolExecutor
from temporalio.worker import Worker
with ThreadPoolExecutor(max_workers=100) as executor:
worker = Worker(
client,
task_queue="my-queue",
workflows=[MyWorkflow],
activities=[my_sync_activity],
activity_executor=executor,
)
await worker.run()
```
### Asynchronous Activities
- Share the default asyncio event loop with the Temporal worker
- Any blocking call freezes the entire loop
- Require async-safe libraries throughout
```python
@activity.defn
async def my_async_activity(name: str) -> str:
# Must use async-safe libraries only
async with aiohttp.ClientSession() as session:
async with session.get(f"http://api.example.com/{name}") as response:
return await response.text()
```
## HTTP Libraries: A Critical Choice
| Library | Type | Safe in Async Activity? |
|---------|------|------------------------|
| `requests` | Blocking | No - blocks event loop |
| `urllib3` | Blocking | No - blocks event loop |
| `aiohttp` | Async | Yes |
| `httpx` | Both | Yes (use async mode) |
**Example: Wrong way (blocks event loop)**
```python
@activity.defn
async def bad_activity(url: str) -> str:
import requests
response = requests.get(url) # BLOCKS the event loop!
return response.text
```
**Example: Correct way (async-safe)**
```python
@activity.defn
async def good_activity(url: str) -> str:
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
```
## Running Blocking Code in Async Activities
If blocking code must run in an async activity, offload it to a thread:
```python
import asyncio
@activity.defn
async def activity_with_blocking_call() -> str:
# Run blocking code in a thread pool
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, blocking_function)
return result
# Or use asyncio.to_thread (Python 3.9+)
@activity.defn
async def activity_with_blocking_call_v2() -> str:
result = await asyncio.to_thread(blocking_function)
return result
```
## When to Use Async Activities
Use async activities only when:
1. All code paths are async-safe (no blocking calls)
2. Using async-native libraries (aiohttp, asyncpg, motor, etc.)
3. Performance benefits are needed for I/O-bound operations
4. The team understands async constraints
## When to Use Sync Activities
Use sync activities when:
1. Making HTTP calls with `requests` or similar blocking libraries
2. Performing file I/O operations
3. Using database drivers that aren't async-native
4. Uncertain whether code is async-safe
5. Integrating with legacy or third-party synchronous code
## Debugging Tip
If experiencing sporadic bugs, hangs, or timeouts:
1. Convert async activities to sync
2. Test thoroughly
3. If bugs disappear, the original async activity had blocking calls
## Threading Considerations
### Multi-Core Usage
For CPU-bound work and multi-core usage:
- Prefer multiple worker processes and/or threaded synchronous activities.
- Use ProcessPoolExecutor for synchronous activities only if you understand and accept the extra complexity and different cancellation semantics.
### Separate Workers for Workflows vs Activities
Some teams deploy:
- Workflow-only workers (CPU-bound, need deadlock detection)
- Activity-only workers (I/O-bound, may need more parallelism)
This prevents resource contention and allows independent scaling.
## Complete Example: Sync Activity with ThreadPoolExecutor
```python
import urllib.parse
import requests
from concurrent.futures import ThreadPoolExecutor
from temporalio import activity
from temporalio.client import Client
from temporalio.worker import Worker
@activity.defn
def greet_in_spanish(name: str) -> str:
"""Synchronous activity using requests library."""
url = f"http://localhost:9999/get-spanish-greeting?name={urllib.parse.quote(name)}"
response = requests.get(url)
return response.text
async def main():
client = await Client.connect("localhost:7233", namespace="default")
with ThreadPoolExecutor(max_workers=100) as executor:
worker = Worker(
client,
task_queue="greeting-tasks",
workflows=[GreetingWorkflow],
activities=[greet_in_spanish],
activity_executor=executor,
)
await worker.run()
```
## Complete Example: Async Activity with aiohttp
```python
import aiohttp
import urllib.parse
from temporalio import activity
from temporalio.client import Client
from temporalio.worker import Worker
class TranslateActivities:
def __init__(self, session: aiohttp.ClientSession):
self.session = session
@activity.defn
async def greet_in_spanish(self, name: str) -> str:
"""Async activity using aiohttp - safe for event loop."""
url = f"http://localhost:9999/get-spanish-greeting?name={urllib.parse.quote(name)}"
async with self.session.get(url) as response:
return await response.text()
async def main():
client = await Client.connect("localhost:7233", namespace="default")
async with aiohttp.ClientSession() as session:
activities = TranslateActivities(session)
worker = Worker(
client,
task_queue="greeting-tasks",
workflows=[GreetingWorkflow],
activities=[activities.greet_in_spanish],
)
await worker.run()
```
## Summary
| Aspect | Sync Activities | Async Activities |
|--------|-----------------|------------------|
| Default choice | Yes | Only when certain |
| Blocking calls | Safe (runs in thread pool) | Dangerous (blocks event loop) |
| HTTP library | `requests`, `httpx` | `aiohttp`, `httpx` (async) |
| Executor needed | Yes (`ThreadPoolExecutor`) | No |
| Debugging | Easier | Harder (timing issues) |