Skip to content

Preserve trailing external events after continue-as-new - #281

Open
andystaples wants to merge 4 commits into
mainfrom
andystaples-preserve-trailing-external-events
Open

andystaples wants to merge 4 commits into
mainfrom
andystaples-preserve-trailing-external-events

Conversation

@andystaples

@andystaples andystaples commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #274

Summary

Preserve external events arriving after continue_as_new() instead of delivering them to abandoned waits from the completed execution. This fixes the timer-winning when_any scenario where trailing events in the same batch were lost despite save_events=True.

The event-delivery fix follows the finalized-execution approach in the merged microsoft/durabletask-go#163: undelivered events are buffered for carryover, while events already delivered to live waits are not restored.

Behavior

  • With save_events=True, undelivered events are carried into the next execution once, preserving global arrival order across event names and retaining raw/null payloads. With save_events=False, they are omitted from carryover.
  • Delivery to a live wait consumes the event, even if the orchestrator never yields that wait or it loses a when_any race. Losing waits are not automatically canceled.
  • Python retains its existing completion boundary: continue_as_new() makes the context terminal at the call; Go finalizes when the orchestrator function returns. Python orchestrators should return immediately after calling continue_as_new(). This change does not interrupt synchronous code, buffered-event consumption, or finally cleanup, and does not alter exception-after-continue behavior.
  • Ordinary external events do not complete abandoned waits after normal completion or termination either. Failed and terminated completions do not carry events forward. An unhandled exception still exits history processing; an exception after continue_as_new() does not override Python's already-terminal status.
  • Entity call/lock responses retain their existing routing. No blanket history-event filter or pending-task cancellation is introduced.

Main integration and coverage

Merged the latest main and resolved changelog and regression-test conflicts without dropping either set of changes. The PR now includes the global carryover-order fix from #278 and the terminal timer-callback guard from #280.

Regression coverage includes both save modes, timer/event winners, replay and new-event processing, multiple same-name waits, explicit cancellation, case-insensitive names, raw/null payloads, already-delivered versus undelivered events, terminal lifecycle behavior, entity responses and unlock retention, and serialized Azure Functions worker responses. Added combined coverage for mixed-name trailing events and their consumption in the next execution.

Local validation: 407 affected executor/provider unit tests passed; changed core source passes Pyright; source and test lint passes for all three packages. External-service E2E tests were not run locally; GitHub CI runs against the updated head.

Purge discrimination (#276) remains out of scope.

Release coordination

Unreleased notes are included in all three package changelogs. No package versions or dependency minimums change here. Release the corrected core first, then coordinate provider minimum dependencies in a dedicated release PR.

Gate ordinary external-event delivery on the existing terminal context boundary while retaining entity routing and raw carryover buffering.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Track buffered event arrival indexes independently of task IDs and retain per-name FIFO consumption and raw payloads. Add replay, selective consumption, cancellation, converter, and save-events regression coverage.

Fixes #277

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@andystaples
andystaples marked this pull request as ready for review October 1, 2026 22:52
Copilot AI balanced review requested due to automatic review settings October 1, 2026 22:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The implementation appears consistent, but the draft explicitly requires human agreement on cross-SDK event-delivery semantics.

Review effort: Balanced
Findings: None

What changed in this PR

Preserves trailing external events after terminal orchestration states, especially continue_as_new.

Changes:

  • Gates delivery to abandoned event waits after completion.
  • Documents event-consumption semantics.
  • Adds comprehensive core/provider regression coverage and changelogs.
File Description
durabletask/​worker.py Buffers events after completion.
durabletask/​task.py Documents continue-as-new semantics.
tests/​durabletask/​test_orchestration_executor.py Covers lifecycle and carryover cases.
tests/​azure-functions-durable/​test_worker_compat.py Verifies serialized provider responses.
CHANGELOG.md Records the core fix.
durabletask-azuremanaged/​CHANGELOG.md Records managed-provider impact.
azure-functions-durable/​CHANGELOG.md Records Functions-provider impact.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

andystaples and others added 2 commits October 1, 2026 18:04
Guard timer continuation at the executor callback boundary while preserving pre-terminal actions and trailing history processing. Cover terminal states, replay, native/chunked retries, cancellation, and Functions compatibility.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Preserve both event-delivery and global-order regression coverage, and verify carried events are consumed by the next execution.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
save_events: bool, replay_completion: bool):
waits: list[task.CancellableTask[Any]] = []

def orchestrator(ctx: task.OrchestrationContext, input: str | None):

@berndverst Bernd Verst (berndverst) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the updated implementation and main integration. No actionable findings. The completion-state guard preserves trailing external events without restoring events already delivered to live waits, while retaining existing Python continue-as-new semantics, cancellation behavior, and entity-response routing.

@andystaples
andystaples dismissed Bernd Verst (berndverst)’s stale review October 5, 2026 16:36

The merge-base changed after approval.

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.

Preserve trailing external events after continue-as-new with pending losing waits

3 participants