feat(DurableExecution): incremental, heterogeneous Parallel API (#2519) - #2553
feat(DurableExecution): incremental, heterogeneous Parallel API (#2519)#2553GarrettBeatty wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Replay validation, cancellation, completion concurrency, and branch result consistency contain unresolved correctness issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an incremental parallel API supporting heterogeneous typed branches while preserving existing batch APIs and checkpoint formats.
Changes:
- Adds
CreateParallel, typed branch handles, and orchestration logic. - Extracts shared batch-summary serialization.
- Adds unit, integration, and documentation coverage.
File summaries
| File | Description |
|---|---|
IncrementalParallelOperationTests.cs |
Tests incremental execution and replay. |
IncrementalParallelReplayFunction.csproj |
Configures replay test function. |
IncrementalParallelReplayFunction/Function.cs |
Exercises replay paths. |
IncrementalParallelHeterogeneousFunction.csproj |
Configures heterogeneous test function. |
IncrementalParallelHeterogeneousFunction/Function.cs |
Exercises typed branches. |
IncrementalParallelReplayTest.cs |
Validates replay integration. |
IncrementalParallelHeterogeneousTest.cs |
Validates heterogeneous integration. |
IParallelBranch.cs |
Defines typed awaitable handles. |
IncrementalParallelOperation.cs |
Implements incremental orchestration. |
ConcurrentOperation.cs |
Uses shared summary codec. |
BatchSummaryCodec.cs |
Centralizes summary serialization. |
IDurableParallel.cs |
Defines the public parallel API. |
IDurableContext.cs |
Exposes CreateParallel. |
DurableContext.cs |
Constructs incremental operations. |
docs/core/parallel.md |
Documents the new API. |
Review details
Suppressed comments (2)
Libraries/src/Amazon.Lambda.DurableExecution/Internal/IncrementalParallelOperation.cs:160
- Serialize the value before completing the public result task. With
NestingType.Flat,Serialize(value)can throw afterTrySetResult; the catch path then records a failed outcome but cannot replace the already-successful handle result, sosummary.HasFailure/Statusreport failure whileawait branchreturns a value.
var value = await run().ConfigureAwait(false);
if (_frozenStatus is null) _status = (int)BatchItemStatus.Succeeded;
_result.TrySetResult(value);
return BranchOutcome.Success(Index, Name, Serialize(value));
Libraries/src/Amazon.Lambda.DurableExecution/Internal/IncrementalParallelOperation.cs:331
- Handle missing/corrupt terminal summaries explicitly.
ParseSummaryreturns null for these payloads, but the operation remains in Terminal mode; every branch is then resolved as skipped andCompleteAsyncsynthesizesAllCompleted, masking a previously terminal checkpoint. Recover from child checkpoints where possible or fail replay rather than returning a false success.
if (terminal)
{
_mode = ParallelExecutionMode.Terminal;
_frozenSummary = BatchSummaryCodec.ParseSummary(existing!.ContextDetails?.Result);
_startTask = Task.CompletedTask;
- Files reviewed: 16/16 changed files
- Comments generated: 7
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
43ff3ef to
3fd7096
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.
|
|
||
| // The parent operation position has been reached — mirror the base | ||
| // DurableOperation.ExecuteAsync bookkeeping for the parent CONTEXT op. | ||
| _state.ValidateReplayConsistency(_operationId, OperationTypes.Context, _name); |
| $"'{_name ?? _operationId}': expected name '{name}' but found '{summaryEntry.Name}' " + | ||
| $"from a previous invocation. Code must not change the order or name of branches " + |
cfac883 to
545df80
Compare
3fd7096 to
9b1220e
Compare
…al-parallel overflow/await fixes, serializer deferral, Branch rename, docs DurableExecution PR #2553 (stacked on feature/per-step-serializer-conformance). 1. [BLOCKER] StepOperation.ExecuteFunc: move the fresh-success SUCCEED enqueue and result round-trip OUTSIDE the try that funnels into HandleStepFailureAsync (mirrors ChildContextOperation). A serializer that cannot deserialize its own just-written payload now surfaces the fault directly instead of enqueuing a RETRY/FAIL that conflicts with the already-committed SUCCEED. 2. [MAJOR] .autover/changes/12a4a1f7: Minor -> Major. The package is GA (1.x) per CLAUDE.md, and the unconditional fresh-success round-trip is an observable happy-path behavior change for ALL serializers on non-suspending workflows (reference identity, DateTime.Kind, [JsonIgnore], precision). Not preview-exempt. 3. [MAJOR] IncrementalParallelOperation overflow recovery: isolate overflow-recovery re-runs (frozenStatus set) from _shortCircuitCts/_dispatchCts so a completion-policy short-circuit can no longer cancel them; and exclude frozen branches from the cooperative-bail arm so _result honors _frozenStatus (never resolves a frozen Succeeded branch to SkippedError, which made `await branch` throw while Status==Succeeded and lost the recovered value). 4. [MINOR] DurableContext.CreateParallel: defer LambdaSerializerHelper.GetRequired via a lazy factory (memoized in the operation). A workflow overriding the serializer on every branch no longer requires a global serializer at CreateParallel time (AOT / per-branch scenario). GetRequired is resolved only when a branch falls back. 5. [MINOR] Rename IDurableParallel.BranchAsync<T> -> Branch<T>. The method returns a handle synchronously (not a Task), so the Async suffix was misleading. Safe: the API is new/unreleased (absent on master). Updated the interface, impl, all call sites (conformance + tests), docs (parallel.md), and the AutoVer changelog text. 6. [MINOR] IDurableParallel.Branch XML doc: document ArgumentNullException, ObjectDisposedException, and NonDeterministicExecutionException in addition to InvalidOperationException. 7. [MINOR] IncrementalParallelBranch.ExecuteAsync: fault _result before rethrowing a workflow-level DurableExecutionException, so a caller that catches the fault out of CompleteAsync and then awaits the handle observes the fault instead of hanging. 8. [MINOR] Correct the IncrementalParallelOperation class summary and IParallelBranch.Index doc to reflect the 1-based operation-ID suffix (hash("{parentId}-{index+1}")). 9. [NIT] IncrementalParallelHeterogeneousTest: replace the tautological Contains("200") (satisfied by the "USD:4200" POCO branch) with the distinguishing token "Payment":200. Tests: added 4 unit tests (fresh-success round-trip deserialize failure surfaces without RETRY/FAIL; CreateParallel with no global serializer + per-branch overrides does not throw; deferred fallback still throws on a non-overriding branch; a branch faulting with a workflow-level error faults the handle instead of hanging). Build and Amazon.Lambda.DurableExecution.Tests pass (447/447, net10.0). Integration-test and deployed-function projects compile; the heterogeneous integration test requires an AWS deployment and was not run here. AutoVer: source changes are refinements to the two features already covered by the existing change files, so both existing entries were updated (12a4a1f7 -> Major; add-incremental changelog text updated for the Branch rename) rather than adding a new change file. Reclassifying 12a4a1f7's Type was a one-field edit — the AutoVer CLI has no edit verb, and adding a third Major entry would have left the mislabeled Minor in place.
Adds an additive, branch-oriented parallel API alongside the existing
homogeneous ParallelAsync<T> overloads:
await using var parallel = ctx.CreateParallel(name: "process-order");
IParallelBranch<InventoryReservation> inv = parallel.BranchAsync("inventory", ...);
IParallelBranch<PaymentAuthorization> pay = parallel.BranchAsync("payment", ...);
IBatchResult summary = await parallel.CompleteAsync();
InventoryReservation r = await inv; // own type, no shared base/cast/envelope
Each branch declares its own result type (heterogeneous) and returns an
awaitable typed handle; branches are registered incrementally and start
executing on registration (gated by MaxConcurrency); CompleteAsync seals,
awaits per CompletionConfig, and checkpoints the aggregate.
Implementation reuses the existing machinery so replay is identical to the
batch API: each branch runs as a ChildContextOperation<T> with the same
deterministic child op id (hash("{parentId}-{index}")) and the same parent
CONTEXT/Parallel BatchSummary checkpoint shape. Terminal-parent replay
reconstructs branch outcomes from the frozen inline summary (re-running only
overflow-stripped branches); DisposeAsync auto-completes so `await using`
always writes the terminal checkpoint. MaxConcurrency, CompletionConfig,
NestingType, cancellation, and ILambdaSerializer are honored unchanged.
Also factors the BatchSummary (de)serialization + overflow handling out of
ConcurrentOperation<T> into a shared BatchSummaryCodec so the batch and
incremental parallel paths cannot diverge on the wire format.
New public API:
- IDurableContext.CreateParallel(name?, config?)
- IDurableParallel (BranchAsync<T>, CompleteAsync, IAsyncDisposable)
- IParallelBranch<T> (Name/Index/Status, awaitable)
Tests: 16 unit tests (IncrementalParallelOperationTests) covering fresh happy
path, heterogeneous types, deterministic ids, MaxConcurrency, completion
short-circuit/skip, failure surfacing, empty, replay reconstruct, name-drift,
and STARTED-parent replay. Two integration tests (heterogeneous end-to-end and
replay determinism across the Run and Terminal-reconstruct paths), both
verified green against the durable execution service.
All 428 unit tests pass; docs/core/parallel.md documents the new API.
…#2519) - Replay: switch explicitly on parent status — only SUCCEEDED reconstructs and only STARTED/PENDING re-run; any other terminal status (FAILED/CANCELLED/ STOPPED/TIMED_OUT) throws NonDeterministicExecutionException instead of silently re-running and overwriting the prior outcome (mirrors ConcurrentOperation.ReplayAsync). - CompleteAsync idempotence: cache the in-progress completion Task, not just the finished result, so concurrent CompleteAsync/DisposeAsync calls share one completion and enqueue exactly one parent SUCCEED. - Terminal replay: enforce the positional replay contract — the registered branch count must equal the frozen summary's unit count, else throw. - Percentage failure tolerance is no longer evaluated against the incomplete denominator during incremental registration; it is suppressed until the operation is sealed (CompletionPolicy gains an evaluatePercentage flag, defaulting true so batch behavior is unchanged). - Observe the per-branch result-task fault in the handle ctor so a discarded failed handle cannot surface as an UnobservedTaskException. - DisposeAsync no longer throws: its safety-net completion swallows faults. - Docs: correct the CreateParallel `name` param (positional op id, not name-derived); document that the CompleteAsync token governs sealing/awaiting and does not retroactively cancel already-started branch bodies. Adds 3 unit tests (unexpected-status throw, branch-count-mismatch throw, percentage-not-evaluated-before-seal). 431 unit tests pass; both incremental integration tests re-verified green against the durable execution service.
…r CreateParallel (#2519) Stacks on the per-step-serializer work: CreateParallel now honors ParallelConfig.ItemSerializer as the operation-level branch-result serializer, and IDurableParallel.BranchAsync accepts an optional per-branch ILambdaSerializer override (falls back to ItemSerializer, then the globally-registered serializer). Each branch's serializer is threaded into both its ChildContextOperation and the inline summary serialization so fresh and replay values match. Adds unit tests for per-branch and operation-level ItemSerializer, and relaxes the timing- sensitive FirstSuccessful test to its deterministic invariants.
* test(DurableExecution): add incremental parallel conformance handlers * test(DurableExecution): add dedicated static typing suite --------- Co-authored-by: Frank Chen <frankchn@dev-dsk-frankchn-2a-ff9871a5.us-west-2.amazon.com>
…al-parallel overflow/await fixes, serializer deferral, Branch rename, docs DurableExecution PR #2553 (stacked on feature/per-step-serializer-conformance). 1. [BLOCKER] StepOperation.ExecuteFunc: move the fresh-success SUCCEED enqueue and result round-trip OUTSIDE the try that funnels into HandleStepFailureAsync (mirrors ChildContextOperation). A serializer that cannot deserialize its own just-written payload now surfaces the fault directly instead of enqueuing a RETRY/FAIL that conflicts with the already-committed SUCCEED. 2. [MAJOR] .autover/changes/12a4a1f7: Minor -> Major. The package is GA (1.x) per CLAUDE.md, and the unconditional fresh-success round-trip is an observable happy-path behavior change for ALL serializers on non-suspending workflows (reference identity, DateTime.Kind, [JsonIgnore], precision). Not preview-exempt. 3. [MAJOR] IncrementalParallelOperation overflow recovery: isolate overflow-recovery re-runs (frozenStatus set) from _shortCircuitCts/_dispatchCts so a completion-policy short-circuit can no longer cancel them; and exclude frozen branches from the cooperative-bail arm so _result honors _frozenStatus (never resolves a frozen Succeeded branch to SkippedError, which made `await branch` throw while Status==Succeeded and lost the recovered value). 4. [MINOR] DurableContext.CreateParallel: defer LambdaSerializerHelper.GetRequired via a lazy factory (memoized in the operation). A workflow overriding the serializer on every branch no longer requires a global serializer at CreateParallel time (AOT / per-branch scenario). GetRequired is resolved only when a branch falls back. 5. [MINOR] Rename IDurableParallel.BranchAsync<T> -> Branch<T>. The method returns a handle synchronously (not a Task), so the Async suffix was misleading. Safe: the API is new/unreleased (absent on master). Updated the interface, impl, all call sites (conformance + tests), docs (parallel.md), and the AutoVer changelog text. 6. [MINOR] IDurableParallel.Branch XML doc: document ArgumentNullException, ObjectDisposedException, and NonDeterministicExecutionException in addition to InvalidOperationException. 7. [MINOR] IncrementalParallelBranch.ExecuteAsync: fault _result before rethrowing a workflow-level DurableExecutionException, so a caller that catches the fault out of CompleteAsync and then awaits the handle observes the fault instead of hanging. 8. [MINOR] Correct the IncrementalParallelOperation class summary and IParallelBranch.Index doc to reflect the 1-based operation-ID suffix (hash("{parentId}-{index+1}")). 9. [NIT] IncrementalParallelHeterogeneousTest: replace the tautological Contains("200") (satisfied by the "USD:4200" POCO branch) with the distinguishing token "Payment":200. Tests: added 4 unit tests (fresh-success round-trip deserialize failure surfaces without RETRY/FAIL; CreateParallel with no global serializer + per-branch overrides does not throw; deferred fallback still throws on a non-overriding branch; a branch faulting with a workflow-level error faults the handle instead of hanging). Build and Amazon.Lambda.DurableExecution.Tests pass (447/447, net10.0). Integration-test and deployed-function projects compile; the heterogeneous integration test requires an AWS deployment and was not run here. AutoVer: source changes are refinements to the two features already covered by the existing change files, so both existing entries were updated (12a4a1f7 -> Major; add-incremental changelog text updated for the Branch rename) rather than adding a new change file. Reclassifying 12a4a1f7's Type was a one-field edit — the AutoVer CLI has no edit verb, and adding a third Major entry would have left the mislabeled Minor in place.
…c; add overflow-recovery terminal-path tests
9b727b7 to
c760c01
Compare
Description
Implements #2519: an additive, branch-oriented parallel API for
Amazon.Lambda.DurableExecutionsupporting heterogeneous per-branch result types and incremental branch registration, alongside the existing homogeneousParallelAsync<T>overloads (which are unchanged).What & why
Today every branch of a
Parallelmust share one generic result typeT, forcing unrelated branch contracts intoobject, a common base type, or a wrapper. This adds a branch-scoped generic API (matching the Java SDK'sParallelDurableFuture) that gives each branch its own compile-time type, replay-safe per-branch deserialization, and incremental composition (register/start branches as work is discovered, then seal).New public API
IDurableContext.CreateParallel(name?, config?)→IDurableParallelIDurableParallel:IAsyncDisposable—BranchAsync<T>(name, func),CompleteAsync(ct)IParallelBranch<T>— awaitable typed handle exposingName/Index/StatusDesign
ChildContextOperation<T>with the same deterministic child op id (hash("{parentId}-{index}")) and the same parentCONTEXT/ParallelBatchSummarycheckpoint shape as batchParallel— so replay, checkpoints, and reconstruction are identical and interoperable.MaxConcurrencysemaphore and a cooperative short-circuit token;CompleteAsyncseals, awaits perCompletionConfig, and checkpoints the aggregate.NonDeterministicExecutionException. Terminal-parent replay reconstructs from the frozen inline summary without re-running (re-running only overflow-stripped branches).DisposeAsyncauto-completes ifCompleteAsyncwasn't called, soawait usingalways writes the terminal checkpoint.MaxConcurrency,CompletionConfig,NestingType, cancellation, and the registeredILambdaSerializerare honored unchanged.BatchSummary(de)serialization + overflow handling out ofConcurrentOperation<T>into a sharedBatchSummaryCodecso the batch and incremental paths can't diverge on the wire format.Testing
IncrementalParallelOperationTests): fresh happy path, heterogeneous types, deterministic ids,MaxConcurrency, completion short-circuit/skip, failure surfacing, empty, replay reconstruct (inline + failed branch), name-drift, and STARTED-parent replay. All 428 unit tests pass.IncrementalParallelHeterogeneousTest— string/int/POCO branches round-trip end-to-end.IncrementalParallelReplayTest— deterministic replay across both the STARTED-parent Run path and the terminal-reconstruct resume (each branch step executes exactly once).docs/core/parallel.md.Note for reviewers
Because
IParallelBranch<T>is awaitable, a bareparallel.BranchAsync(...)statement whose result is ignored trips CS4014 under the repo's warnings-as-errors — callers must capture or discard (_ =) the handle. Flagging in case the team prefers a non-awaitable handle + explicitGetResultAsync().By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.