Skip to content

fix: complete shared and duplicate whenAll tasks - #383

Open
wangbill (YunchuWang) wants to merge 2 commits into
mainfrom
yunchuwang-shared-task-fan-in
Open

wangbill (YunchuWang) wants to merge 2 commits into
mainfrom
yunchuwang-shared-task-fan-in

Conversation

@YunchuWang

@YunchuWang wangbill (YunchuWang) commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

What changed?

  • Replace destructive single-parent task subscriptions with notifications to every dependent composite.
  • Count duplicate child references without repeating activities, preserving input-order results and duplicate positions.
  • Detach completed whenAny groups from their remaining children. Notify other dependents even when one completion callback throws, preserving a single thrown error and aggregating multiple callback errors.
  • Add public in-memory client/worker regressions and task-graph boundary tests; update the core README and changelog.

Why is this change needed?

  • whenAll([a, b]) and whenAll([a, c]) previously competed for a's parent subscription. The first group could remain pending after all three activities completed.
  • whenAll([t, t]) previously counted one completion against two array positions and remained pending after its only activity completed.
  • Both patterns now complete with the expected results, executing each uniquely scheduled activity once.

Issues / work items

  • Resolves: No linked issue.
  • Related: No linked work item.

Project checklist

  • Release notes are not required for the next release
    • Otherwise: Notes added to CHANGELOG.md
  • Backport is not required
    • Otherwise: Backport tracked by issue/PR (not assessed)
  • All required tests have been added/updated (unit tests, E2E tests)
  • Breaking change?
    • If yes:
      • Impact: Previously shared nested whenAny graphs can select a different direct child and take different branches during replay. Internal task-parent bookkeeping also changes.
      • Migration guidance: Review affected shared race graphs and use orchestration versioning or drain affected instances before changing worker versions.

AI-assisted code disclosure (required)

Was an AI tool used? (select one)

  • No
  • Yes, AI helped write parts of this PR (e.g., GitHub Copilot)
  • Yes, an AI agent generated most of this PR

If AI was used:

  • Tool(s): GitHub Copilot.
  • AI-assisted areas/files: Core task composition and notifications, regression tests, README, changelog, and this description.
  • What you changed after AI output: No human edits or review are attested at preparation.

AI verification (required if AI was used):

  • I understand the code and can explain it
  • I verified referenced APIs/types exist and are correct
  • I reviewed edge cases/failure paths (timeouts, retries, cancellation, exceptions)
  • I reviewed concurrency/async behavior
  • I checked for unintended breaking or behavior changes

Testing

Automated tests

  • Result: Passed locally; hosted CI has not run.
  • Test-first evidence: Before production edits, the new suites had 21 behavioral failures and two passes, with no compiler or infrastructure errors. Both public regressions observed RUNNING after all activity completions were committed.
  • npm test --workspace @microsoft/durabletask-js -- --runTestsByPath test\composite-task.spec.ts test\task-composition.spec.ts test\when-all-task.spec.ts test\when-any-task.spec.ts test\long-timer-replay-regressions.spec.ts --silent: passed, 84 tests in five suites.
  • npm run test:unit --workspace @microsoft/durabletask-js -- --silent: passed, 1,719 tests in 87 core suites.
  • npm run build:core: passed, including the core TypeScript build.
  • npm run example -- .\examples\azure-managed\unit-testing\index.ts: passed, all six in-memory demos.
  • git --no-pager diff --check origin/main...HEAD: passed.

Changed-file checks ran in PowerShell:

$changedTs = @(git diff --name-only origin/main...HEAD -- '*.ts' | ForEach-Object { $_.Replace('/', '\') })
node .\node_modules\eslint\bin\eslint.js @changedTs
node .\node_modules\prettier\bin\prettier.cjs --check --end-of-line auto @changedTs .\CHANGELOG.md
  • ESLint passed for all 12 changed TypeScript files.
  • Prettier passed for those files and the changelog, preserving their existing LF/CRLF styles. Pre-existing whole-file README formatting drift was not reflowed.
  • Not run locally: Full-workspace build/lint/tests, Go-sidecar E2E, DTS-emulator E2E, or emulator-dependent samples. Functions-host E2E does not match the changed paths. No cloud or Functions adapter validation is claimed.

Manual validation (only if runtime/behavior changed)

  • Environment (OS, Node.js version, components): Windows, Node.js v24.14.0; repository-built core SDK, InMemoryOrchestrationBackend, TestOrchestrationClient, and TestOrchestrationWorker.
  • Steps + observed results: These are automated runtime checks, not human manual validation.
    1. Schedule shared groups through the public client and real worker: COMPLETED, output [["A","B"],["A","C"]], three actual activity invocations.
    2. Schedule the same pending task twice in one group: COMPLETED, output ["V","V"], one actual activity invocation.
    3. Verify distinct tasks, nested disjoint groups, and precompleted duplicates; all complete with ordered results. Completion fences require committed activity histories, and workers/clients stop with backend cleanup.
  • Evidence (optional): Committed task-composition.spec.ts uses a reused worker/client and committed-history fences. composite-task.spec.ts covers all six completion orders, mixed/precompleted duplicates, shared nested composites, wait-all failure order, race identity/detachment, cancellation callback errors, and policy/custom retry terminal paths.

Notes for reviewers

  • whenAny returns its winning direct child, not a flattened descendant. For firstRace = whenAny([timer, work]) and secondRace = whenAny([firstRace, timer]), timer completion now notifies firstRace; its notification can make secondRace select firstRace. Previously the timer's parent was overwritten, and firstRace could later select work. The existing shared-timer regression is updated to assert this intentional observable change. Historical winner ordering or unchanged replay branching for shared graphs is not claimed.
  • A canceled child's result still throws, and a whenAll whose result collection throws still does not notify its own parents. Other composites observing that child are nevertheless notified; multiple callback errors surface in an AggregateError.
  • The test that assigned internal _parent state now uses a real composite parent and a notification spy. Supported orchestrator signatures, activity scheduling, wait-all failure aggregation, and protocol/backend behavior are unchanged.
  • This is a standalone core SDK correction, not a claim about a production incident, cloud storage, or backend resource failures. No dependencies, protocol definitions, Functions adapters, Python, or .NET code are changed.

Notify every dependent composite and count duplicate task positions without repeating activities. Release completed race subscriptions and preserve callback errors, wait-all failures, and direct-child winner identity.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 007ee1b8-76d7-478f-a825-ec91e732300f
Copilot AI balanced review requested due to automatic review settings October 7, 2026 17:39

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.

🔵 Needs a closer look

Core task-graph changes alter observable replay branching and require final human validation.

1 open finding
What changed in this PR

Fixes task composition so shared and duplicate tasks complete reliably.

Changes:

  • Adds multi-parent completion notifications and duplicate tracking.
  • Detaches completed whenAny groups from losing children.
  • Adds regression coverage and documentation.
File Description
README.md Documents shared tasks and callback errors.
CHANGELOG.md Adds release note.
src/​task/​task.ts Implements multi-parent notification.
src/​task/​composite-task.ts Tracks unique children and occurrences.
src/​task/​completable-task.ts Notifies all parents.
src/​task/​when-all-task.ts Handles duplicate references.
src/​task/​when-any-task.ts Uses multi-parent notification.
src/​task/​retry-task-base.ts Updates retry failure notification.
src/​task/​timer-task.ts Updates cancellation documentation.
src/​task/​index.ts Revises whenAll API documentation.
test/​composite-task.spec.ts Adds task-graph boundary tests.
test/​task-composition.spec.ts Adds public integration regressions.
test/​when-all-task.spec.ts Uses a real composite parent.
test/​long-timer-replay-regressions.spec.ts Updates nested-race expectations.

🧠 Review effort: Balanced


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

Comment on lines +9 to +13
* Returns a task that completes after every provided task completes or fails.
* Results preserve input order and duplicate task references. Tasks may be shared between groups.
*
* @param tasks the tasks to wait for
* @returns {WhenAllTask} a task that completes when all of the provided tasks complete or when one of the tasks fail
* @returns {WhenAllTask} a task containing the ordered results, or an aggregate of child failures
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.

4 participants