Skip to content

Commit b68683d

Browse files
committed
cleanup
Signed-off-by: Filinto Duran <[email protected]>
1 parent d31a1e3 commit b68683d

24 files changed

+177
-793
lines changed

README.md

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ export DAPR_WF_DISABLE_DETERMINISTIC_DETECTION=false
358358

359359
### Async workflow authoring
360360

361-
For a deeper tour of the async authoring surface (determinism helpers, sandbox modes, timeouts, concurrency patterns), see the Async Enhancements guide: [ASYNC_ENHANCEMENTS.md](./ASYNC_ENHANCEMENTS.md). The developer-facing migration notes are in [DEVELOPER_TRANSITION_GUIDE.md](./DEVELOPER_TRANSITION_GUIDE.md).
361+
For a deeper tour of the async authoring surface (determinism helpers, sandbox modes, timeouts, concurrency patterns), see the Async Enhancements guide: [ASYNC_ENHANCEMENTS.md](./ASYNC_ENHANCEMENTS.md).
362362

363363
You can author orchestrators with `async def` using the new `durabletask.aio` package, which provides a comprehensive async workflow API:
364364

@@ -376,9 +376,11 @@ with TaskHubGrpcWorker() as worker:
376376
worker.add_orchestrator(my_orch)
377377
```
378378

379-
Optional sandbox mode (`best_effort` or `strict`) patches `asyncio.sleep`, `random`, `uuid.uuid4`, and `time.time` within the workflow step to deterministic equivalents. This is best-effort and not a correctness guarantee.
379+
The sandbox (enabled by default) patches standard Python functions to deterministic equivalents during workflow execution. This allows natural async code like `asyncio.sleep()`, `random.random()`, and `asyncio.gather()` to work correctly with workflow replay. Three modes are available:
380380

381-
In `strict` mode, `asyncio.create_task` is blocked inside workflows to preserve determinism and will raise a `SandboxViolationError` if used.
381+
- `"best_effort"` (default): Patches functions, minimal overhead
382+
- `"strict"`: Patches + blocks dangerous operations (file I/O, `asyncio.create_task`)
383+
- `"off"`: No patching (requires manual use of `ctx.*` methods everywhere)
382384

383385
> **Enhanced Sandbox Features**: The enhanced version includes comprehensive non-determinism detection, timeout support, enhanced concurrency primitives, and debugging tools. See [ASYNC_ENHANCEMENTS.md](./durabletask/aio/ASYNCIO_ENHANCEMENTS.md) for complete documentation.
384386
@@ -406,13 +408,10 @@ val = await ctx.wait_for_external_event("approval")
406408
- Concurrency:
407409
```python
408410
t1 = ctx.call_activity("a"); t2 = ctx.call_activity("b")
409-
await ctx.when_all([t1, t2])
410-
winner = await ctx.when_any([ctx.wait_for_external_event("x"), ctx.sleep(5)])
411-
412-
# gather combines awaitables and preserves order
413-
results = await ctx.gather(t1, t2)
414-
# gather with exception capture
415-
results_or_errors = await ctx.gather(t1, t2, return_exceptions=True)
411+
# when_all waits for all tasks and returns results in order
412+
results = await ctx.when_all([t1, t2])
413+
# when_any returns (index, result) tuple of first completed task
414+
idx, result = await ctx.when_any([ctx.wait_for_external_event("x"), ctx.create_timer(5)])
416415
```
417416

418417
#### Async vs. generator API differences
@@ -457,15 +456,6 @@ except Exception as e:
457456
...
458457
```
459458

460-
Or capture with gather:
461-
462-
```python
463-
res = await ctx.gather(ctx.call_activity("a"), return_exceptions=True)
464-
if isinstance(res[0], Exception):
465-
...
466-
```
467-
468-
469459
- Sub-orchestrations (function reference or registered name):
470460
```python
471461
out = await ctx.call_sub_orchestrator(child_fn, input=payload)
@@ -477,20 +467,6 @@ out = await ctx.call_sub_orchestrator(child_fn, input=payload)
477467
now = ctx.now(); rid = ctx.random().random(); uid = ctx.uuid4()
478468
```
479469

480-
- Workflow metadata/headers (async only for now):
481-
```python
482-
# Attach contextual metadata (e.g., tracing, tenant, app info)
483-
ctx.set_metadata({"x-trace": trace_id, "tenant": "acme"})
484-
md = ctx.get_metadata()
485-
486-
# Header aliases (same data)
487-
ctx.set_headers({"region": "us-east"})
488-
headers = ctx.get_headers()
489-
```
490-
Notes:
491-
- Useful for routing, observability, and cross-cutting concerns passed along activity/sub-orchestrator calls via the sidecar.
492-
- In python-sdk, available for both async and generator orchestrators. In this repo, currently implemented on `durabletask.aio`; generator parity is planned.
493-
494470
- Cross-app activity/sub-orchestrator routing (async only for now):
495471
```python
496472
# Route activity to a different app via app_id

durabletask/aio/ASYNCIO_ENHANCEMENTS.md

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,14 @@ with TaskHubGrpcWorker() as worker:
6363

6464
### 2. **Non-Determinism Detection**
6565
- Automatic detection of non-deterministic function calls
66-
- Three modes: `"off"` (default), `"best_effort"` (warnings), `"strict"` (errors)
66+
- Three modes: `"best_effort"` (default), `"strict"` (errors), `"off"` (no patching)
6767
- Comprehensive coverage of problematic functions
6868
- Helpful suggestions for deterministic alternatives
6969

7070
### 3. **Enhanced Concurrency Primitives**
71-
- `when_any_with_result()` - Returns (index, result) tuple
71+
- `when_all()` - Waits for all tasks to complete and returns list of results in order
72+
- `when_any()` - Returns (index, result) tuple indicating which task completed first
7273
- `with_timeout()` - Add timeout to any operation
73-
- `gather(*awaitables, return_exceptions=False)` - Compose awaitables:
74-
- Preserves input order; returns list of results
75-
- `return_exceptions=True` captures exceptions as values
76-
- Empty gather resolves immediately to `[]`
77-
- Safe to await the same gather result multiple times (cached)
7874

7975
### 4. **Async Context Management**
8076
- Full async context manager support (`async with ctx:`)
@@ -125,52 +121,39 @@ Note: The `sandbox_mode` parameter accepts both `SandboxMode` enum values and st
125121
Control non-determinism detection with the `sandbox_mode` parameter:
126122

127123
```python
128-
# Production: Zero overhead (default)
129-
worker.add_orchestrator(workflow, sandbox_mode="off")
124+
# Default: Patches asyncio functions for determinism, optional warnings
125+
worker.add_orchestrator(workflow) # Uses "best_effort" by default
130126

131-
# Development: Warnings for non-deterministic calls
127+
# Development: Same as default, warnings when debug mode enabled
132128
worker.add_orchestrator(workflow, sandbox_mode=SandboxMode.BEST_EFFORT)
133129

134130
# Testing: Errors for non-deterministic calls
135131
worker.add_orchestrator(workflow, sandbox_mode=SandboxMode.STRICT)
132+
133+
# No patching: Use only if all code uses ctx.* methods explicitly
134+
worker.add_orchestrator(workflow, sandbox_mode="off")
136135
```
137136

138-
Why enable detection (briefly):
139-
- Catch accidental non-determinism in development (BEST_EFFORT) before it ships.
140-
- Keep production fast with zero overhead (OFF).
141-
- Enforce determinism in CI (STRICT) to prevent regressions.
137+
Why "best_effort" is the default:
138+
- Makes standard asyncio patterns work correctly (asyncio.sleep, asyncio.gather, etc.)
139+
- Patches random/time/uuid to be deterministic automatically
140+
- Optional warnings only when debug mode is enabled (low overhead)
141+
- Provides "pit of success" for async workflow authoring
142142

143143
### Performance Impact
144-
- `"off"`: Zero overhead (recommended for production)
145-
- `"best_effort"/"strict"`: ~100-200% overhead due to Python tracing
144+
- `"best_effort"` (default): Minimal overhead from function patching. Tracing overhead present but uses lightweight noop tracer unless debug mode is enabled.
145+
- `"strict"`: ~100-200% overhead due to full Python tracing for detection
146+
- `"off"`: Zero overhead (no patching, no tracing)
146147
- Global disable: Set `DAPR_WF_DISABLE_DETERMINISTIC_DETECTION=true` environment variable
147148

149+
Note: Function patching overhead is minimal (single-digit percentage). Tracing overhead (when enabled) is more significant due to Python's sys.settrace() mechanism.
150+
148151
## Environment Variables
149152

150153
- `DAPR_WF_DEBUG=true` / `DT_DEBUG=true` - Enable debug logging, operation tracking, and non-determinism warnings
151154
- `DAPR_WF_DISABLE_DETERMINISTIC_DETECTION=true` - Globally disable non-determinism detection
152155

153156
## Developer Mode
154-
## Workflow Metadata and Headers (Async Only)
155-
156-
Purpose:
157-
- Carry lightweight key/value context (e.g., tracing IDs, tenant, app info) across workflow steps.
158-
- Enable routing and observability without embedding data into workflow inputs/outputs.
159-
160-
API:
161-
```python
162-
md_before = ctx.get_metadata() # Optional[Dict[str, str]]
163-
ctx.set_metadata({"tenant": "acme", "x-trace": trace_id})
164-
165-
# Header aliases (same data for users familiar with other SDKs)
166-
ctx.set_headers({"region": "us-east"})
167-
headers = ctx.get_headers()
168-
```
169-
170-
Notes:
171-
- In python-sdk, metadata/headers are available for both async and generator orchestrators; this repo currently implements the asyncio path.
172-
- Metadata is intended for small strings; avoid large payloads.
173-
- Sidecar integrations may forward metadata as gRPC headers to activities and sub-orchestrations.
174157

175158
Set `DAPR_WF_DEBUG=true` during development to enable:
176159
- Non-determinism warnings for problematic function calls
@@ -206,14 +189,8 @@ async def workflow_with_timeout(ctx: AsyncWorkflowContext, input_data) -> str:
206189
return result
207190
```
208191

209-
### Enhanced when_any
210-
Note: `when_any` still exists. `when_any_with_result` is an addition for cases where you also want the index of the first completed.
192+
### when_any with index and result
211193

212-
```python
213-
# Both forms are supported
214-
winner_value = await ctx.when_any(tasks)
215-
winner_index, winner_value = await ctx.when_any_with_result(tasks)
216-
```
217194
```python
218195
async def competitive_workflow(ctx, input_data):
219196
tasks = [
@@ -222,8 +199,8 @@ async def competitive_workflow(ctx, input_data):
222199
ctx.call_activity("provider_c")
223200
]
224201

225-
# Get both index and result of first completed
226-
winner_index, result = await ctx.when_any_with_result(tasks)
202+
# when_any returns (index, result) tuple
203+
winner_index, result = await ctx.when_any(tasks)
227204
return f"Provider {winner_index} won with: {result}"
228205
```
229206

@@ -258,9 +235,9 @@ async def workflow_with_cleanup(ctx, input_data):
258235
- `ctx.random()` instead of `random`
259236
- `ctx.uuid4()` instead of `uuid.uuid4()`
260237

261-
2. **Enable detection during development**:
238+
2. **Use strict mode in testing**:
262239
```python
263-
sandbox_mode = "best_effort" if os.getenv("ENV") == "dev" else "off"
240+
sandbox_mode = "strict" if os.getenv("CI") else "best_effort"
264241
```
265242

266243
3. **Add timeouts to external operations**:

durabletask/aio/ASYNCIO_INTERNALS.md

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,25 @@ Optional Sandbox (per activation):
140140

141141
## Sandboxing and Non‑Determinism Detection
142142

143-
The sandbox provides optional, scoped compatibility and detection for common non‑deterministic stdlib calls. It is opt‑in per orchestrator via `sandbox_mode`:
143+
The sandbox provides scoped compatibility and detection for common non‑deterministic stdlib calls. It is configured per orchestrator via `sandbox_mode`:
144144

145-
- `off` (default): No patching or detection; zero overhead. Use deterministic APIs only.
146-
- `best_effort`: Patch common functions within a scope and emit warnings on detected non‑determinism.
147-
- `strict`: As above, but raise `SandboxViolationError` on detected calls.
145+
- `best_effort` (default): Patch common functions within a scope and emit warnings on detected non‑determinism when debug mode is enabled.
146+
- `strict`: Patch common functions and raise `SandboxViolationError` on detected calls.
147+
- `off`: No patching or detection; zero overhead. Use deterministic APIs only.
148148

149-
Patched targets (best‑effort):
149+
Patched targets (best‑effort and strict):
150150
- `asyncio.sleep` → deterministic timer awaitable
151-
- `random` module functions (via a deterministic `Random` instance)
151+
- `asyncio.gather` → replay-safe one-shot awaitable wrapper using WhenAllAwaitable
152+
- `random` module functions (random, randrange, randint, getrandbits via deterministic PRNG)
152153
- `uuid.uuid4` → derived from deterministic PRNG
153154
- `time.time/time_ns` → orchestration time
154155

156+
Additional blocks in strict mode only:
157+
- `asyncio.create_task` → raises SandboxViolationError
158+
- `builtins.open` → raises SandboxViolationError
159+
- `os.urandom` → raises SandboxViolationError
160+
- `secrets.token_bytes/token_hex` → raises SandboxViolationError
161+
155162
Important limitations:
156163
- `datetime.datetime.now()` is not patched (type immutability). Use `ctx.now()` or `ctx.current_utc_datetime`.
157164
- `from x import y` may bypass patches due to direct binding.
@@ -174,23 +181,21 @@ Modes and behavior:
174181
- `SandboxMode.OFF`:
175182
- No tracing, no patching, zero overhead
176183
- Detector is not active
177-
- `SandboxMode.BEST_EFFORT`:
178-
- Patches selected stdlib functions
179-
- Installs tracer only when `ctx._debug_mode` is true; otherwise a no‑op tracer is used to keep overhead minimal
184+
- `SandboxMode.BEST_EFFORT` (default):
185+
- Patches selected stdlib functions (asyncio.sleep, random, uuid.uuid4, time.time, asyncio.gather)
186+
- Installs tracer only when `ctx._debug_mode` is true; otherwise no tracer (minimal overhead)
180187
- Emits `NonDeterminismWarning` once per unique callsite with a suggested deterministic alternative
181188
- `SandboxMode.STRICT`:
182-
- Patches selected stdlib functions and blocks dangerous operations (e.g., `open`, `os.urandom`, `secrets.*`)
189+
- Patches selected stdlib functions and blocks dangerous operations (e.g., `open`, `os.urandom`, `secrets.*`, `asyncio.create_task`)
183190
- Installs full tracer regardless of debug flag
184191
- Raises `SandboxViolationError` on first detection with details and suggestions
185192

186-
When to use it (recommended):
187-
- During development to quickly surface accidental non‑determinism in orchestrator code
188-
- When integrating third‑party libraries that might call time/random/uuid internally
189-
- In CI for a dedicated “determinism” job (short test matrix), using `BEST_EFFORT` for warnings or `STRICT` for enforcement
193+
When to use each mode:
194+
- `BEST_EFFORT` (default): Recommended for most use cases. Patches make standard asyncio patterns work correctly with minimal overhead.
195+
- `STRICT`: Use in CI/testing to enforce determinism and catch violations early.
196+
- `OFF`: Use only if you're certain all code uses `ctx.*` methods exclusively and want absolute zero overhead.
190197

191-
When not to use it:
192-
- Production environments (prefer `OFF` for zero overhead)
193-
- Performance‑sensitive local loops (e.g., microbenchmarks) unless you are specifically testing detection overhead
198+
Note: `BEST_EFFORT` is now the default because it makes workflows "just work" with standard asyncio code patterns.
194199

195200
Enabling and controlling the detector:
196201
- Per‑orchestrator registration:
@@ -220,9 +225,11 @@ What warnings/errors look like:
220225
- Includes violation type, suggested alternative, `workflow_name`, and `instance_id` when available
221226

222227
Overhead and performance:
223-
- `OFF`: zero overhead
224-
- `BEST_EFFORT`: minimal overhead by default; full detection overhead only when debug is enabled
225-
- `STRICT`: tracing overhead present; recommended only for testing/enforcement, not for production
228+
- `OFF`: zero overhead (no patching, no detection)
229+
- `BEST_EFFORT` (default): minimal overhead from patching; lightweight noop tracer unless debug mode enabled (full detection tracer only when `DAPR_WF_DEBUG=true`)
230+
- `STRICT`: ~100-200% overhead due to full Python tracing; recommended only for testing/enforcement
231+
232+
Note: The patching overhead (module-level function replacement) is minimal. The tracing overhead (sys.settrace) is more significant when full detection is enabled.
226233

227234
Limitations and caveats:
228235
- Direct imports like `from random import random` bind the function and may bypass patching

durabletask/aio/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
TimeoutAwaitable,
1919
WhenAllAwaitable,
2020
WhenAnyAwaitable,
21-
WhenAnyResultAwaitable,
22-
gather,
2321
)
2422
from .client import AsyncTaskHubGrpcClient
2523

@@ -60,10 +58,8 @@
6058
"ExternalEventAwaitable",
6159
"WhenAllAwaitable",
6260
"WhenAnyAwaitable",
63-
"WhenAnyResultAwaitable",
6461
"TimeoutAwaitable",
6562
"SwallowExceptionAwaitable",
66-
"gather",
6763
# Sandbox and utilities
6864
"SandboxMode",
6965
"_NonDeterminismDetector",

0 commit comments

Comments
 (0)