Add opt-in guard for duplicate awaited sub-orchestration IDs - #1402
wangbill (YunchuWang) wants to merge 2 commits into
Conversation
Fail conflicting decision batches before scheduling work, while preserving default-off compatibility and sequential instance ID reuse. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3bb13ddc-65b4-45c0-b262-c2d7b51fbe91
There was a problem hiding this comment.
🟡 Changes recommended
Move the three test files into the active test/ projects so they are compiled and executed.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds an opt-in Core guard against duplicate IDs among concurrently awaited sub-orchestrations.
Changes:
- Adds worker configuration and dispatcher preflight validation.
- Emits non-retriable parent failures for conflicting batches.
- Documents behavior and adds regression tests.
File summaries
| File | Summary |
|---|---|
Test/DurableTask.Core.Tests/SubOrchestrationInstanceIdValidatorTests.cs |
Validator tests; currently outside the active lowercase test/ project. |
Test/DurableTask.Core.Tests/DuplicateSubOrchestrationDispatcherTests.cs |
Dispatcher tests; currently outside the active lowercase test/ project. |
Test/DurableTask.AzureStorage.Tests/DuplicateSubOrchestrationInstanceIdTests.cs |
Azure Storage tests; currently outside the active lowercase test/ project. |
src/DurableTask.Core/TaskOrchestrationDispatcher.cs |
Validates complete decision batches before dispatch. |
src/DurableTask.Core/TaskHubWorker.cs |
Adds the default-off opt-in setting. |
src/DurableTask.Core/SubOrchestrationInstanceIdValidator.cs |
Detects conflicting pending child IDs. |
docs/features/sub-orchestrations.md |
Documents configuration, behavior, and limitations. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🔵 Needs a closer look
The new tests reside under uppercase Test/ and are not compiled by the active lowercase test/ projects on case-sensitive systems.
Review details
Suppressed comments (3)
Test/DurableTask.Core.Tests/SubOrchestrationInstanceIdValidatorTests.cs:25
- This test is under
Test/, but the solution referencestest/DurableTask.Core.Tests/DurableTask.Core.Tests.csproj(DurableTask.sln:26), and that SDK project does not link sources from the uppercase tree. On case-sensitive build agents this file is therefore never compiled or discovered, leaving the validator behavior untested. Move it into the lowercasetest/DurableTask.Core.Tests/project directory.
[TestClass]
public class SubOrchestrationInstanceIdValidatorTests
Test/DurableTask.Core.Tests/DuplicateSubOrchestrationDispatcherTests.cs:31
- This test is under
Test/, whileDurableTask.sln:26builds the project from lowercasetest/DurableTask.Core.Tests/; that project has no source link to this uppercase directory. Consequently these dispatcher regressions are skipped on case-sensitive systems. Move the file intotest/DurableTask.Core.Tests/.
[TestClass]
public class DuplicateSubOrchestrationDispatcherTests
Test/DurableTask.AzureStorage.Tests/DuplicateSubOrchestrationInstanceIdTests.cs:30
- This file is outside the active Azure Storage test project:
DurableTask.sln:42points to lowercasetest/DurableTask.AzureStorage.Tests/, whose project does not include sources fromTest/. On case-sensitive CI none of these provider-level scenarios execute. Move the file into the lowercase test project directory.
[TestClass]
public class DuplicateSubOrchestrationInstanceIdTests
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Build the pending index lazily from reduced history, maintain it for accepted events, and keep unsent decisions separate. Document explicit invalidation for custom history rewrites and correct canonical test paths and null-flow handling. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3bb13ddc-65b4-45c0-b262-c2d7b51fbe91
There was a problem hiding this comment.
🔵 Needs a closer look
The changes affect shared dispatcher semantics and cached runtime-state behavior across providers, warranting final human review.
Review details
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Summary
Related to Azure/azure-functions-durable-extension#1947. This is a Core prerequisite, not automatic resolution of the Functions issue.
TaskHubWorker.FailOnDuplicateSubOrchestrationInstanceIds, defaulting to false, configured beforeStartAsync.DuplicateSubOrchestrationInstanceIddetails. Discard the whole conflicting batch, including unrelated activities, timers, events, and children.OrchestrationRuntimeState. Lazily reduce cold history to surviving task-ID/instance-ID pairs before creating pending nodes, then maintain it incrementally through acceptedAddEventcalls. Proposed actions have a separate temporary batch map and never poison the accepted-history index if rejected, unsent, split, or retried.test/...Git paths and use an explicit non-null pattern guard for failure-notification assertions.This avoids faulting only one task in
Task.WhenAllwhile another child remains pending, and covers middleware that returns raw orchestrator actions without using the in-process context.Compatibility and lifecycle contract
Eventslist and serialized payload shape. Custom history rewriters should construct a freshOrchestrationRuntimeState, or callInvalidateSubOrchestrationInstanceIdIndex()after direct list edits or accepted indexed identity/correlation/fire-and-forget changes. Arbitrary external mutations are not detected automatically. The hook clears only derived tracking, not other runtime metadata.Local validation
Assertions inspect actual outbound messages and persisted history, not only child invocations. Coverage includes whole-batch suppression, message splitting, proposal isolation, duplicate old completions, legacy linked-node removal/overwrite, no repeated history scan, new-event maintenance, default-off/startup capture, mutation invalidation, unchanged Objects/Auto/All JSON, stream restore, ContinueAsNew, actual rewind, suspension, failed checkpoint/abandon/retry, parent failure propagation, and sequential/retry reuse.
AzureStorage event-gated tests cover same-batch and later-episode conflicts without releasing the held child, plus distinct/case-sensitive/automatic IDs and sequential reuse after success/failure, with extended sessions off/on. Rejected starts are absent from persisted history/instances and queues. Only unique local task hubs were used; owned emulators were stopped.
Verified the exact canonical Git paths and actual MSBuild Compile items on Windows and on an exported Git tree in a case-sensitive Linux filesystem. Linux Compile-item evaluation succeeded using existing managed SDK binaries; a full Linux build/test run was not performed.
Independent local .NET isolated Functions confirmation used the unchanged official isolated worker, the final locally patched host Core DLL, and temporary test-only host-extension option wiring. Six guard-on cases passed (same-batch/cross-episode rejection, distinct/automatic IDs, success/failure reuse). The same corrected host binaries with the guard off reproduced both original duplicate-ID hangs, retaining one valid completion and a different missing task correlation after a probe replay. This is local causal evidence, not published Functions support or a CI claim.
Synthetic helper performance
Matched old helper (
2e9b8ef) versus indexed implementation; optimized Release/net8, seven-repetition medians, prebuilt inputs, shared host. These are helper CPU-bound elapsed time and managed allocations, not production end-to-end percentages.Progressive reused-state runs of 1k/2k/4k sequential children, including accepted create and completion event/index maintenance, took 0.386/0.742/1.444 ms versus 17.32/68.83/276.57 ms for the old helper plus the same event bookkeeping. This is approximately linear incremental helper work, not a claim that all orchestration execution becomes linear.
The costs are not free: validating and accepting a fresh 10k-child fanout took about 1.877 ms versus 0.940 ms before. New acceptance/index maintenance allocated 2.36 MB; still-pending cold loads also construct retained nodes. Disabled/no-action paths allocated 0 B. Cold initialization and upfront acceptance costs are reported separately rather than hidden by warm-only numbers.
Core command (repeat with
-f net48):AzureStorage selector with an explicitly local
DurableTaskTestStorageConnectionString: