Skip to content

[Async stack 1/6] Coroutine dispatch - #357

Draft
andystaples wants to merge 4 commits into
andystaples-async-stack-0-contract-docsfrom
andystaples-async-stack-1-coroutine-dispatch
Draft

andystaples wants to merge 4 commits into
andystaples-async-stack-0-contract-docsfrom
andystaples-async-stack-1-coroutine-dispatch

Conversation

@andystaples

@andystaples andystaples commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Do not merge this PR, or any other PR in the stack, on its own. The whole stack will be squash-merged once every PR in it is approved.

Layer 1/6 of the async orchestrator stack, which splits the closed #293 for review. It is based on #356.

Scope

  • shared.is_async_callable classifies orchestrator callables. Each callable is checked directly first: coroutine functions are async and generator functions are synchronous, even when they carry functools.wraps metadata from a function of the other kind. Partials and callable objects resolve through func and __call__; only a callable that is neither falls back to the function it wraps. Cyclic __wrapped__ chains are reported as errors.
  • The worker registry classifies each orchestrator once, at registration, so wrapper cycles fail when the orchestrator is registered rather than on every dispatch.
  • Validation at the point where the worker calls the orchestrator:
    • A sync callable that returns a coroutine is rejected, and the coroutine is closed.
    • An async-classified callable that returns something other than a coroutine is rejected with guidance about functools.wraps. If it returned a generator, that generator is closed.
    • Async generators are rejected.
    • Generators that yield anything other than a Task are rejected.
  • The worker runs coroutine orchestrators and resumes them during replay through a minimal deterministic DurableEventLoop (durabletask/internal/asyncio_loop.py). This loop is enough to:
    • drive the root coroutine
    • suspend it on durable tasks
    • deliver completions
    • fail when the orchestrator is blocked on work that isn't durable
  • loop.time() is derived from the replay-safe orchestration clock (naive or timezone-aware). Loop timers, including asyncio.sleep() with a positive delay, fail with an error that points to ctx.create_timer(...) until [Async stack 2/6] asyncio composition and durable timers #358 adds durable loop timers.
  • The module docstring of asyncio_loop.py lists each private asyncio API the loop depends on and why.
  • Task.__await__ and Task.wait() are now concrete. Inside the durable loop, awaiting suspends on the durable task. With no running loop, __await__ yields the task itself so mock-based drivers can send results. Under a different running event loop, it raises RuntimeError.
  • Adds the SyncOrchestrator, AsyncOrchestrator, and Orchestrator aliases.
  • Generator orchestrator behavior is unchanged, and a new test replays a generator history in the form earlier releases record it. Teardown now always closes the orchestrator, whether it is a generator or a coroutine.

Temporary differences (resolved in later layers)

@andystaples
andystaples added this pull request to stack #365 November 11, 2026 20:39
@andystaples andystaples changed the title [Async stack 1/7] Coroutine dispatch [Async stack 1/6] Coroutine dispatch Nov 11, 2026
@andystaples
andystaples force-pushed the andystaples-async-stack-1-coroutine-dispatch branch from e01cc4a to 1c76780 Compare November 12, 2026 16:48
Comment thread durabletask/worker.py Resolved
andystaples and others added 2 commits November 12, 2026 13:22
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Track identities across wrapped, partial, and callable nodes; recognize intermediate coroutine functions before following their wrapped targets. Cover bounded worker failures and nested decorated dispatch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@andystaples
andystaples force-pushed the andystaples-async-stack-1-coroutine-dispatch branch from 1c76780 to e73a705 Compare November 12, 2026 20:23
andystaples and others added 2 commits November 12, 2026 15:33
…stack-1-coroutine-dispatch

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Check each callable before following functools.wraps metadata so generator
wrappers of async functions stay synchronous, and classify once when the
orchestrator is registered so wrapper cycles fail at registration. Normalize
timezone-aware orchestration clocks in loop.time(), give loop timers an
actionable error, document the private asyncio APIs the loop depends on, and
add a generator history replay compatibility test. Remove the changelog entry
for unreleased behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant