Repository navigation
preserve trailing external events across continue-as-new - #163
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
We found the same trailing-event loss in Python and opened microsoft/durabletask-python#281. That PR is deliberately draft while we discuss the cross-SDK contract. The Go fix looks sound; this is a non-blocking semantics discussion, not a request to hold the immediate fix. Shared reproducer and consumption edge casesA timer wins The important distinctions are:
Where Python deliberately differs, and why
The Python guard applies only to ordinary external-event delivery; entity call/lock responses retain their earlier routing. It neither cancels every pending task nor stops processing all trailing history. We also reproduced cross-name reordering ( Cross-SDK questions
It would be useful to document the intended observable event-retention contract across Durable, while allowing language-specific execution/cleanup mechanics where necessary. Python's draft and regression coverage make the current differences explicit rather than silently declaring a new standard. |

The problem
An orchestrator can use
WhenAnyto wait for either an external event or a timer. If the timer wins, it can callContinueAsNewand return while the event wait is still pending.The same batch can contain a matching event after the timer. The SDK delivered that event to the old wait—even though the old run had finished and could never resume. The event was lost instead of being kept for the next run, despite
WithKeepUnprocessedEventsbeing enabled.We reproduced the loss on both the DTS emulator and live DTS. With the fix, the next run received all six test events once and in order.
The fix
ContinueAsNew.WithKeepUnprocessedEventsis enabled, preserving their arrival order.What this means for event handling
Calling
ContinueAsNewdoes not immediately end the current run. The orchestrator can still receive events before it returns.Only undelivered events are kept. An event already delivered to a live wait is not carried forward, even if application code never awaits that wait. Without
WithKeepUnprocessedEvents, undelivered events are dropped as before.WhenAnystill does not cancel its losing waits. Cancel a losing event wait explicitly, or useEventChannelwithSelect, if later code should receive that event.