fix: align long durable timers and cancellation with Python - #366
Merged
Merged
Conversation
Add configurable deterministic timer segments while preserving logical task identity and cancellation. Enable safe Functions defaults, retain native DTS timers, and bound in-memory Node timeouts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Broad cross-package behavior changes and missing live backend validation require final human review.
Pull request overview
Adds backend-aware segmentation for long durable timers and retry delays across core, Functions, testing, and Azure Managed integrations.
Changes:
- Adds configurable timer intervals and provider-specific defaults.
- Preserves replay, cancellation, and logical task identity across segments.
- Bounds native in-memory timeouts and adds tests, documentation, and changelogs.
File summaries
| File | Description |
|---|---|
README.md |
Documents timer policies and rollout guidance. |
packages/durabletask-js/test/long-timers.spec.ts |
Tests segmented timers, retries, cancellation, replay, and validation. |
packages/durabletask-js/test/in-memory-long-timers.spec.ts |
Tests bounded native timeouts. |
packages/durabletask-js/src/worker/task-hub-grpc-worker.ts |
Propagates timer options to execution. |
packages/durabletask-js/src/worker/runtime-orchestration-context.ts |
Schedules timer and retry segments. |
packages/durabletask-js/src/worker/orchestration-executor.ts |
Advances segments during replay. |
packages/durabletask-js/src/worker/durable-timer-options.ts |
Defines and validates timer configuration. |
packages/durabletask-js/src/testing/test-worker.ts |
Supports timer options in test workers. |
packages/durabletask-js/src/testing/in-memory-backend.ts |
Re-arms oversized native timeouts. |
packages/durabletask-js/src/index.ts |
Exports timer options. |
packages/durabletask-js-azuremanaged/test/unit/worker-builder.spec.ts |
Verifies native DTS timers. |
packages/durabletask-js-azuremanaged/src/worker-builder.ts |
Retains native timers for DTS. |
packages/durabletask-js-azuremanaged/CHANGELOG.md |
Documents Azure Managed behavior. |
packages/azure-functions-durable/test/unit/worker.spec.ts |
Tests Functions timer policies. |
packages/azure-functions-durable/test/unit/testing.spec.ts |
Tests testing-helper timer propagation. |
packages/azure-functions-durable/test/unit/app.spec.ts |
Tests app setup and registration behavior. |
packages/azure-functions-durable/src/worker.ts |
Applies the Functions timer default. |
packages/azure-functions-durable/src/testing/index.ts |
Propagates timer options to test runs. |
packages/azure-functions-durable/src/app.ts |
Adds configurable shared-worker setup. |
packages/azure-functions-durable/README.md |
Documents Functions timer configuration. |
packages/azure-functions-durable/CHANGELOG.md |
Records provider timer changes. |
CHANGELOG.md |
Records core timer and timeout changes. |
Review details
- Files reviewed: 22/22 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Retain both timer and history-streaming release notes and preserve upstream worker shutdown behavior. Add streamed-history coverage for deterministic timer segments. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecc7780b-7549-48d5-99df-42ba05748db8
Use fixed three-day segments in Functions and its test helper through protected provider hooks. Remove public timer configuration and restore eager app registration while preserving native core/DTS behavior and replay semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecc7780b-7549-48d5-99df-42ba05748db8
Default core and test workers to three-day segments, support the Python-equivalent interval override, and keep AzureManaged native. Make timer cancellation terminal with boolean return and TaskCancelledError results. Remove provider hooks and the separate Node timeout re-arm change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecc7780b-7549-48d5-99df-42ba05748db8
Use the exact durabletask-go revision from the last successful Node 22/24 E2E run. Upstream main removed the root sidecar executable, breaking bootstrap before tests could start. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecc7780b-7549-48d5-99df-42ba05748db8
Cover interleaved legacy histories, boundaries, cancellation ordering, nested composites, lifecycle and retry replay. Assert a 30-day logical timer produces a three-day Azure Storage host history segment and cancels on activity completion. Document exceptional composite reuse and custom result-accessor limits. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecc7780b-7549-48d5-99df-42ba05748db8
Prevent a caught timer-cancellation callback followed by yielding whenAll from completing with an undefined result. Preserve pending/failure checks, synchronous callback errors, normal result arrays, and existing parent notification semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ecc7780b-7549-48d5-99df-42ba05748db8
kaibocai (kaibocai)
approved these changes
Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
What changed?
Align durable timer behavior with
microsoft/durabletask-pythonata6c18f24, without adding a provider configuration framework.maximumTimerIntervalMs;null, zero, or negative disables segmentationnull, matching Python's native DTS timersrunOrchestratorTimerFired.fireAt, normal completion only at the final deadlinetrueonce,falsewhen already terminalgetResult()andresultthrowTaskCancelledErrorwhen canceled; timerresultalso throws while pending/failedisCanceledretains its existing JavaScript spelling.whenAnycan return a canceled timer as its winner.whenAllcounts cancellation as terminal and can throw while collecting final child results. Executor resumption now readsgetResult()rather than the raw result field, including for custom Task subclasses; such accessors must be replay-safe.Representation difference: Python
timedeltasupports microseconds; JavaScriptDatesupports milliseconds. Positive fractional intervals round up to whole milliseconds (minimum 1 ms). Non-finite values are rejected; no positive-only/safe-integer restriction is imposed.No total business-timer duration cap, app setup API, provider boolean hooks, private Functions test-worker subclass, or automatic backend detection. Functions connected to DTS still segments. The separate Node timeout re-arm change was removed; native in-memory timers retain the pre-existing Node timeout limit when segmentation is disabled.
Why is this change needed?
The Functions protobuf/gRPC route bypasses legacy host-side long-timer splitting. A single 21/30-day timer can exceed Azure Storage's seven-day queue visibility-delay limit. SDK segments address that constraint without changing the logical deadline or task identity.
Regression found and fixed during adversarial audit
This workflow produced a false success in the initial implementation:
Direct execution confirmed: baseline remained pending, the initial PR completed with
"FALSE_SUCCESS", and pinned Python rejected its uninitialized result. The fix is a nine-lineWhenAllTask.getResult()guard: perform existing pending/failure checks first, then reject an uninitialized result. Valid arrays, including[]and[undefined], remain valid. It preserves the original synchronous cancellation exception, timer state, failure aggregation, and parent-notification behavior. Three focused assertions were observed failing before the fix and passing afterward.The inherited exceptional-parent limitation remains: after a completion callback throws, do not catch it and reuse that group or its parents. A group may be marked complete before its result/parent notification finishes. The guard prevents the false-success read; it does not resume missing notifications or introduce a cancellation framework. Python also leaves the outer parent pending on this path; exact post-exception error types are not claimed identical.
Issues / work items
Related: Azure/azure-functions-durable-extension#1910. This addresses the JavaScript provider gap, not the entire cross-language umbrella issue.
Minimal CI bootstrap repair
Run 34900479259 failed before tests because durabletask-go#158 removed the root sidecar executable. The workflow install and matching local reproduction comment now pin
3fe35d93fe1d2bdab21a3d85c14867532adef0b0, the exact revision from the last passing run. No assertions or skips were changed to resolve that infrastructure failure.Project checklist
Migration: core timers now default to three-day segments and canceled timers are terminal. Timer
resultno longer silently returnsundefinedwhile pending/canceled/failed. InspectisCanceledbefore reading a race winner. Explicit result reads in orchestration code can catchTaskCancelledError; yielding a canceled task fails through the executor's result-read boundary.Old single native timers replay at their recorded deadlines in the tested histories, including overlapping timers and interleaved activity/entity IDs. This is not a universal migration guarantee: changed cancellation branching, mixed versions, or changing intervals can affect replay. Drain affected instances or use a new task hub before rollback, interval changes, or switching providers. Never replay already-segmented histories with native timers.
AI-assisted code disclosure (required)
Was an AI tool used? (select one)
No
Yes, AI helped write parts of this PR
Yes, an AI agent generated most of this PR
Tool: GitHub Copilot App / CLI
Areas: implementation, regressions, directly related docs/changelogs, and the two-file CI bootstrap repair
Revisions: aligned defaults/cancellation to pinned Python; removed provider-only policy machinery and unrelated Node timeout work; reproduced and fixed the caught-cancellation false-success regression.
Agent verification, separate from author review:
Testing
No real Azure cloud resources or production services were provisioned or tested. Local Functions host/Azurite and Docker DTS emulator runs are not Azure-cloud validation, and no test waited 30 days.
Fresh CI on
7f2a478edfe1374f4cdaafb092a930a1affd0cc7Local audit
npm run test:unit: 111 suites / 1,987 passed after the guard fix (core 1,519; Functions 175; AzureManaged 227; history export 66).npm run lint,git diff --check, commit hooks, and all three package builds passed.npm run build:core && npm run build:azuremanaged && npm run build -w durable-functions.Long timer host proof
TimeoutOrchestratoris exercised with 2-second, 10-second, and 30-day timeouts. For 30 days, the test queries actual host history and verifies exactly oneTimerCreatedat orchestration-start + 3 days, noTimerFired, the preserved logical deadline at + 30 days, andisCanceled=true/isCompleted=trueafter the roughly five-second activity wins. This verifies initial long-timer scheduling and cancellation against the Azure Storage host path, not a full multi-day wait.Additional adversarial regressions
long-timer-replay-regressions.spec.tscontains 33 cases, including:preserves overlapping timer/activity/entity IDs through wakeups and nested composites;The narrow guard also has explicit pending, valid
[undefined](before/after construction), empty-array, and ordinary failure coverage.Eight baseline/head workflows produced byte-identical protobuf actions without changing the primary checkout: short timer + activity; overlapping native timers; caught activity failure; policy retry; nested composite success; canceled losing timeout followed by an activity; suspend/resume of a short timer; continue-as-new carryover/version. These checks provide evidence for the exercised flows, not a proof of zero regressions.
Notes for reviewers
Pinned references: task/cancellation/composites, timer segmentation, AzureManaged native override, Functions inherited default.
App registration, the in-memory backend, base Task, and WhenAny implementations remain unchanged. WhenAll has only the result-read guard described above. No npm dependency/protocol/generated-source changes; upstream history streaming and response cancellation from #363 remain intact.