Skip to content

Add deterministic async/await orchestrators across all three Python packages - #293

Draft
andystaples wants to merge 18 commits into
mainfrom
andystaples-async-orchestrator-spike
Draft

andystaples wants to merge 18 commits into
mainfrom
andystaples-async-orchestrator-spike

Conversation

@andystaples

@andystaples andystaples commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements the coordinated async authoring contract discussed in #294 across durabletask, durabletask.azuremanaged, and azure-functions-durable. Existing synchronous generator orchestrators remain supported. This stays one draft PR and one customer-facing release unit; it must not be merged as a partial feature.

Shared runtime and supported contract

  • Native async def orchestrators and nested helpers await existing durable activities, timers, external events, sub-orchestrations, combinators, and supported entity operations. Generator/coroutine parents and children interoperate. Functions supports native two-argument and compatibility single-argument entry points.
  • A dispatch-local AbstractEventLoop supplies native asyncio Task/Future semantics and FIFO scheduling on top of the existing history/retry/payload/entity engine, not a separate provider runtime. Each history completion resumes its ready branches immediately, rather than waiting for a whole batch.
  • Ordered asyncio.gather, local tasks/futures, ready callbacks, and sleep(0) are supported. loop.time() uses the durable clock; call_at/call_later create durable timers, including asyncio.sleep, wait_for, and Python 3.11+ timeout/timeout_at.
  • Python 3.11+ TaskGroup, and the actual AnyIO asyncio task-group/cancel-scope/shielded-cleanup/fail_after paths, use native task semantics. Task-group cancellation wakeups are sorted by task creation rather than object hashes. gather fails fast without cancelling siblings; durable when_all retains wait-all behavior.
  • Local waiter cancellation does not cancel a durable activity, child, entity operation or event subscription, and does not undo external side effects. An unhandled root-coroutine cancellation fails deterministically with TaskCancelledError; scheduler termination remains a client operation.

The full supported contract and migration guidance are in docs/async-orchestrators.md.

Lifecycle and current-main integration

The branch integrates current main, including the dispatch-finalization/exceptional-exit behavior from #300, #303, #306 and #307. The response and entity-release obligations are captured before teardown. Local tasks are cancelled/drained, nested cooperative finalizers and async context managers run, and tracked async generators are closed. New durable work is rejected during teardown. Ordinary cleanup errors and unobserved background-task errors are reported without changing the decided response; unexpected BaseException exits retain the existing failure/recovery policy. Cleanup is bounded, and one cleanup failure does not hide other task errors.

Tests exercise suspension, termination, continue-as-new, history failure, cancellation, action ordering, retries, long/tiny timers, shared waiters, outer event-loop restoration, payload behavior, and generator regressions. Cleanup at a dispatch boundary is explicitly not workflow cancellation or a durable compensation boundary.

Real framework proof

Checked Tamir's integration branch; its head still matches reviewed snapshot 8e9ae67a492171b7839a94f72ff44238f03d597a. The SDK proof uses an actual Pydantic AI Agent.run, deterministic TestModel, and its parallel-tool AnyIO path, covering success, recorded tool failure, and repeated identical-history replay without paid model calls. The agent runs directly inside the orchestrator without an adapter event loop, trampoline, or batch-wide join.

The small proof adapter deliberately retains ordinary model/tool activities, registration, JSON codecs, stable IDs/deadline clock, and canonicalization of volatile framework message metadata. This is not a production Pydantic adapter or blanket compatibility promise. The test-only framework dependency is conditional on Python 3.11+; no framework dependency was added to the shipped SDK.

Deliberate exclusions and limitations

  • No local/virtual activities, arbitrary-await interception, or automatic checkpointing of helper return values. Helpers rerun during replay; is_replaying is not result recording.
  • No general external I/O, threads, executors, subprocesses, custom/eager task factories, or cross-thread scheduling inside orchestrators. Common detectable violations raise actionable errors; not all unsafe operations can be detected.
  • asyncio.wait and as_completed were explicitly investigated and are not deterministic supported composition APIs because of unordered sets/hash-dependent task creation and tie ordering. Successful execution alone is not support.
  • No blanket asyncio/AnyIO/library compatibility. Activities remain at least once and need appropriate idempotency/deduplication.
  • Generator-to-coroutine migration of in-flight instances requires preserving IDs/order, names, payloads, timers/retries/events and entity releases; sequential parity is not blanket migration safety.
  • Functions' mock orchestrator_generator_wrapper handles direct durable awaits, not simulation of the asyncio scheduler.

Validation

Final implementation head: 8c050a7c1ca11b0372151592151f8776796e4485. Local evidence (overlapping suites, not an additive total):

  • Full core run with Azurite: 1,511 passed, 3 skipped.
  • Focused asyncio/coroutine/Pydantic regressions: 55 passed on Python 3.14; 51 passed, 4 skipped on Python 3.10 (version-gated framework/TaskGroup APIs).
  • Azure Managed service-free suite: 192 passed; DTS emulator suite: 86 passed, 1 skipped. Final mixed-style gather/durable-timer emulator probes: 2 passed.
  • Functions unit suite: 420 passed. Full real Functions-host E2E suite: 86 passed, 6 skipped in each payload-typing mode, default and strict.
  • Repository-standard strict Pyright checks pass for core/Azure Managed and Functions with zero errors/warnings. Package source/tests Flake8, changed-document Markdown lint, and diff checks pass.

All final-head CI checks pass, including:

Deployed Azure cloud-service validation has not been performed. Reported skips remain visible in the test results; green CI is not a blanket claim of arbitrary library or migration compatibility.

Release coordination

All three affected changelogs and entry-point documentation are updated. Package versions and dependency minimums are unchanged. The PR remains draft for review as requested. Nothing has been merged or released. Once the full contract is approved, publish core first through the coordinated release process and update provider minimums/versions in the dedicated release PR so resolvers cannot select an incompatible core.

Make durable tasks awaitable, drive native coroutines through the existing replay engine, and preserve generator compatibility. Include provider smoke coverage, prototype documentation, and coordinated-release notes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* feat(functions): add opt-in Unicode JSON serialization

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 54e13dcd-4b0d-4c0b-ab5b-bbb958fd5792

* chore(functions): adopt released Unicode codec API

Require azure-functions 2.4.0b1 and exercise its released opt-in codec directly. Align documentation and samples with the available shared-library API.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 54e13dcd-4b0d-4c0b-ab5b-bbb958fd5792

---------

Co-authored-by: Bernd Verst <beverst@microsoft.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 54e13dcd-4b0d-4c0b-ab5b-bbb958fd5792
Give each replay dispatch explicit ownership of its orchestrator generator: close it once in an outer finally after the response is built, so cleanup in finally blocks runs deterministically and cannot alter the returned actions or custom status. Ordinary teardown failures are logged without replacing dispatch results or abandonment.

Async bridges such as Pydantic AI otherwise retain AnyIO task scopes, event loops, and orchestration contexts indefinitely. Also relevant to the async/await work in #293 and #294.

Fixes #299

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35371373-86cf-4897-bf7c-460fa3c88d1f
…s CancelledError (#303)

Catch asyncio.CancelledError raised by orchestrator generator teardown alongside ordinary cleanup exceptions, so a cancelled async bridge cannot replace an already-built dispatch response or a version-rejection abandonment. SystemExit and KeyboardInterrupt still propagate.

Document the per-dispatch finalizer contract in docs/supported-patterns.md and clarify the three Unreleased changelog entries.

Closes #301

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 35371373-86cf-4897-bf7c-460fa3c88d1f
Fixes #298

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: c95a41ba-3f18-427d-8a6c-a7438e7ec782
* fix: complete permanent payload failures and release entity locks

Fixes #297

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 064f1284-f637-4ba9-8406-8c094ce9f845

* fix: preserve historical entity release order during replay

Retain legacy shared-list ordering while tracking pending lock release obligations independently.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 064f1284-f637-4ba9-8406-8c094ce9f845

* test: explain handled entity failure in replay regression

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 064f1284-f637-4ba9-8406-8c094ce9f845

* fix: keep normal unlock emission separate from terminal cleanup

Preserve pending-call legacy release and subsequent action IDs while retaining missing obligations for permanent payload failure snapshots.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 064f1284-f637-4ba9-8406-8c094ce9f845

* test: make payload failure regression return explicit

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 064f1284-f637-4ba9-8406-8c094ce9f845

---------

Copilot-Session: 064f1284-f637-4ba9-8406-8c094ce9f845
Closes #222.

- Adds 	ask_hub_names: list[str] | None = None to OrchestrationQuery.
- Serializes 	ask_hub_names into InstanceQuery.taskHubNames as 
epeated google.protobuf.StringValue in �uild_query_instances_req().
- Updates unit tests covering 	ask_hub_names serialization independently, combined with instance_id_prefix, and verifying positional argument order preservation.
- Documents the addition in CHANGELOG.md under Unreleased.
* fix(worker): recover unexpected orchestration dispatch exits

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

* fix(worker): preserve recovery signals and stacked lock-release paths

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

* test: simplify dispatch recovery parameter IDs

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

* test: preserve post-call release order in teardown failures

Regression for the pending replay-compatible #306 dependency update; not published until the verified replacement base is integrated.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

* test: verify scheduler-visible teardown failure over local gRPC

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

* test: cover compatible release order for pending entity calls

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

* fix(worker): recover output serialization exits before completion

Keep terminal callbacks non-reentrant while restoring failure conversion after incomplete output serialization. Treat serialization StopIteration as failure, not generator completion.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

* fix(worker): retain pending releases for unexpected terminal failures

Reuse the existing failed-result conversion after the full pre-close snapshot; preserve historical normal timeout actions and original failure details.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1

---------

Copilot-Session: 974213a5-9101-41b4-9bde-aa86a72f25e1
…309)

* fix(entities): implement __hash__ and __repr__ for EntityInstanceId

* fix(entities): enforce immutability on EntityInstanceId for hash stability

* fix(entities): preserve subclass type when pickling
Bumps the github-actions group with 4 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action), [actions/setup-node](https://github.com/actions/setup-node) and [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `github/codeql-action/init` from 4.38.2 to 4.39.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@2892aa5...6f2ac81)

Updates `github/codeql-action/analyze` from 4.38.2 to 4.39.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@2892aa5...6f2ac81)

Updates `actions/setup-node` from 7.0.0 to 7.1.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@8207627...949feb2)

Updates `actions/upload-artifact` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@043fb46...cf430e0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…sonDataConverter (#310)

* fix(serialization): support set and frozenset generic containers in JsonDataConverter

* fix(serialization): deterministic set ordering, opt-in set reconstruction, and pyright typing

- Preserve replay compatibility by gating generic set reconstruction behind enable_generic_sets=False default on JsonDataConverter
- Ensure deterministic set serialization with fallback sort key on recursive encoded values, qualname, and repr
- Retain primitive set element annotations in type discovery when set reconstruction is enabled
- Resolve strict pyright errors and update tests

* fix(serialization): stabilize nested set ordering

* fix(serialization): simplify set serialization by emitting sorted canonical JSON strings
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve generator dispatch recovery and snapshots; resume coroutine branches per history completion, order cancellation wakeups, and define the bounded async contract.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Handle closure tasks created during cleanup, retaining warnings for ordinary errors and terminal recovery for unexpected BaseException exits.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Exercise TestModel parallel tools and recorded failures without a trampoline, retain adapter serialization and replay-stable metadata, and cover Windows Python versions in CI.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
timer = ctx.create_timer(timedelta(days=1))

async def waiter() -> None:
await timer
andystaples and others added 2 commits October 30, 2026 12:50
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
if not future.done():
try:
future.get_coro().close()
except BaseException as error:
@andystaples andystaples changed the title Prototype async/await orchestrators across all three Python packages Add deterministic async/await orchestrators across all three Python packages Oct 30, 2026
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.

Support async/await orchestrators across the Python SDK packages

4 participants