feat(DurableExecution): incremental, heterogeneous Parallel API (#2519) - #2553
feat(DurableExecution): incremental, heterogeneous Parallel API (#2519)#2553GarrettBeatty wants to merge 12 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.
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.
…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.
9b727b7 to
c760c01
Compare
… fix branch name-drift message Address Copilot review on #2553: - CreateParallel 'name' XML docs said a name change 'does not break replay', but the name is passed to ValidateReplayConsistency (throws on drift). Doc now states the name is part of the deterministic definition and must stay stable. - Branch name-drift NonDeterministicExecutionException message had expected/found inverted; now reports the checkpointed name as expected and the current registration as the drifted value.
… fix branch name-drift message Address Copilot review on #2553: - CreateParallel 'name' XML docs said a name change 'does not break replay', but the name is passed to ValidateReplayConsistency (throws on drift). Doc now states the name is part of the deterministic definition and must stay stable. - Branch name-drift NonDeterministicExecutionException message had expected/found inverted; now reports the checkpointed name as expected and the current registration as the drifted value.
51d7258 to
b244c21
Compare
* feat(DurableExecution): add per-operation serializer override Add an optional `ILambdaSerializer? Serializer` to StepConfig, CallbackConfig, InvokeConfig, WaitForConditionConfig<TState>, and ChildContextConfig. DurableContext resolves `config?.Serializer ?? ILambdaContext.Serializer` per operation; the operation classes are otherwise unchanged. When Serializer is null (default), behavior is identical to today (the globally-registered serializer). Boundary serialization (workflow input, handler return, service envelopes) is untouched. Reuses the existing ILambdaSerializer contract (no new SerDes interface); AOT-safe because T stays concrete at each call site. Additive / non-breaking. - Unit tests (PerOperationSerializerTests) covering per-op serialize, default fallback, and replay-deserialize for Step/Callback/Invoke/ChildContext/WaitForCondition. - Integration test (TestFunctions/PerOperationSerializerFunction + PerOperationSerializerTest) verifying per-step routing end-to-end (camelCase override vs PascalCase default). - docs/core updates and an AutoVer change file (Minor). Separate feature from #2540 (FileSystem offload). Phase 1 (single-result ops); Map/Parallel two-level slots are a follow-up (Phase 2). * feat(DurableExecution): add per-item serializer for Map/Parallel Add an optional `ILambdaSerializer? ItemSerializer` to MapConfig<TItem> and ParallelConfig. DurableContext.RunMap/RunParallel resolve `config.ItemSerializer ?? ILambdaContext.Serializer` and pass it as the serializer used for per-item/branch results (each unit's child-context checkpoint and the inline copy on the operation's summary). The aggregated batch envelope (per-unit statuses + completion reason) is a source-generated structure and is unchanged — there is no separate whole-result serializer (matches the Java SDK). Additive / non-breaking. - Unit tests: Map_WithItemSerializer, Map_NoItemSerializer, Parallel_WithItemSerializer. - docs/core (parallel.md, steps.md) + AutoVer change file. Phase 2 of the per-operation serializer feature (Phase 1 = single-result ops). * test(DurableExecution): cloud integration test for Map/Parallel ItemSerializer Deploys MapParallelItemSerializerFunction and asserts from event history that map items and a parallel branch configured with a camelCase ItemSerializer produce camelCase per-item child-context result payloads, while a control step (global serializer) stays PascalCase. Validates Phase 2 end-to-end on AWS. * address review: consistent serializer resolution, config headers, serializer unit coverage - DurableContext: Parallel now resolves the effective serializer via LambdaSerializerHelper.GetRequired(LambdaContext) like every other operation, instead of an inline 'LambdaContext.Serializer ?? throw' (consistent error path). - Add the standard copyright/SPDX header to MapConfig.cs and WaitForConditionConfig.cs. - Add unit tests: Invoke fresh (non-replay) path serializes the request payload via the per-op serializer; Map/Parallel ItemSerializer is used to deserialize cached per-item/per-branch results on replay. * test(DurableExecution): per-op serdes conformance + fresh-success serializer round-trip (#2556) * test(DurableExecution): conformance handlers for per-item/result serdes (map 9-14, parallel 8-15, invoke 5-16) Add conformance handlers exercising the new per-operation serializer slots: - map/MapCustomSerdes (9-14): MapConfig.ItemSerializer wraps each item result - parallel/ParallelCustomSerdes (8-15): ParallelConfig.ItemSerializer wraps each branch result - invoke/InvokeCustomResultSerdes (5-16): InvokeConfig.Serializer uppercases the result on deserialize Remove the corresponding NotImplemented declarations and wire the resources in template_map/parallel/invoke.yaml. All three pass against real AWS via the runner. Packaging fix: add Conformance/Directory.Build.props supplying the SDK's runtime NuGet dependencies (AWSSDK.Lambda, Microsoft.Extensions.Logging.Abstractions) to every handler, since those transitive package assets do not flow into a handler's net8.0 framework-dependent publish (WrapAsyncCore threw FileNotFoundException at runtime). Drop the now-redundant per-handler AWSSDK.Lambda references. * feat(DurableExecution): round-trip step/child results through the serializer on fresh success On a fresh (non-replay) success, StepOperation and ChildContextOperation now return the value deserialized from the just-written checkpoint instead of the original in-memory object, matching replay semantics. This makes a custom per-operation serializer's transform observable in the operation result on the first execution, not only on replay (a non-round-tripping serializer previously had no effect on the fresh result). Overflow (replay-children) child results are unaffected — the payload is stripped and the value is recovered by re-execution. Behavior change documented in the AutoVer change file. * test(DurableExecution): real per-op serdes conformance handlers for step 1-6 and child 3-14 Refactor StepCustomSerdes (1-6) to use a real StepConfig.Serializer (uppercase-on-serialize) instead of transforming inside the step body, and add child/ChildCustomSerdes (3-14) using ChildContextConfig.Serializer. Both rely on the fresh-success round-trip so the serialize-side transform reaches the result. Remove the 3-14 NotImplemented and wire the resource in template_child.yaml. Both pass against real AWS via the runner. * address review: harden conformance serde handlers - Custom serializers now guard typeof(T) == typeof(string) and throw a clear NotSupportedException for unsupported result types (instead of an opaque InvalidCastException from the (T)(object) cast). - Read with an explicit UTF-8 StreamReader for symmetry with the UTF-8 bytes written. - MapCustomSerdes: drop the async-without-await iteration lambda (CS1998); return Task.FromResult(...) so no per-item async state machine is allocated. - InvokeCustomResultSerdes: correct the header comment — the serializer serializes the outbound request payload normally on the initial execution and applies the uppercase transform on deserialize (which, for a chained invoke, happens on replay). The handlers still transform the raw serialized payload, which is the behavior the conformance requirements assert (e.g. invoke 5-16 ExpectedResult '"HELLO"'); the string path is byte-identical, so the suite results are unchanged. * fix(DurableExecution): mark fresh-success round-trip change Major The fresh-success serializer round-trip changes the observable return value (fresh deserialized instance instead of the object the body produced, and a non-round-tripping serializer's transform now visible on the first run). At the released 1.0.0 baseline this is a breaking change and must be Major, not Minor. * chore(DurableExecution): normalize 12a4a1f7 change file (strip trailing newline) to match canonical content
…l determinism (#2559) Introduces the serializer engine that the FileSystemSerializer builds on, split out of the original filesystem PR (#2558) for reviewability: - Optional IDurableResultSerializer + DurableSerializationContext (EntityId + DurableExecutionArn) and LambdaSerializerHelper dispatch; plain ILambdaSerializer serializers are unaffected (byte-identical fallback). - Extends the fresh-success serializer round-trip (established for Step/Child in the conformance work) to Map/Parallel Flat per-item results, eliminating a fresh-vs-replay divergence for non-round-tripping ItemSerializers. - Map/Parallel Nested parent now inlines each child's ORIGINAL SUCCEED payload verbatim instead of re-serializing the round-tripped value. - Terminal handling: a fresh-success round-trip deserialize failure (Step / Child / Flat) is terminal (no retry, body never re-run); Step post-SUCCEED enqueue failure routed through the same terminal path. These changes are triggered by the per-operation serializer feature (#2555), not by FileSystemSerializer specifically; FileSystemSerializer is the leaf that exercises them. Note: two <see cref="FileSystemSerializer"/> doc links are rendered as <c>...</c> here and restored to cref links in the FileSystemSerializer PR that introduces the type.
…to a filesystem (#2540) (#2558) * feat(DurableExecution): FileSystemSerializer for offloading large results Adds FileSystemSerializer (implements ILambdaSerializer + IDurableResultSerializer): stores a durable operation's serialized result on a filesystem (EFS / S3 Files, NOT Lambda /tmp) and keeps only a small {file}|{data} envelope in the checkpoint. Wraps an inner ILambdaSerializer so callers control the on-the-wire format; supports Always/Overflow storage modes and Uri/Hash path encoding; envelope is source-generated (AOT-safe). The plain ILambdaSerializer path throws (offload needs the durable context). Stacked on the serializer-engine PR; restores the two <see cref="FileSystemSerializer"/> doc links now that the type exists. Includes the FileSystemSerializer AutoVer (Minor) change file and unit tests. * address review: inner-less FileSystemSerializer ctor uses global serializer normj asked for a constructor that omits the inner ILambdaSerializer and falls back to the durable execution's globally-registered serializer. Adds FileSystemSerializer(basePath, storageMode, pathEncoding) which leaves _inner null. The durable runtime binds the global serializer as the inner when it resolves the effective per-operation serializer, via a new internal IDefaultInnerSerializer capability (DurableContext.WithDefaultInner at the Step/ChildContext/WaitForCondition/Parallel/Map resolution sites). An explicitly-supplied inner always wins; used without a bound inner it throws a clear InvalidOperationException.
…constructor Add a 'FileSystemSerializer' subsection to the steps custom-serializer docs covering large-result offload to a durable mount, both the inner-taking and the new inner-less constructor (runtime binds the globally-registered serializer as the inner), storage/path-encoding modes, and the retention caveat.
147d7a4 to
075c4d8
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.
… fix branch name-drift message Address Copilot review on #2553: - CreateParallel 'name' XML docs said a name change 'does not break replay', but the name is passed to ValidateReplayConsistency (throws on drift). Doc now states the name is part of the deterministic definition and must stay stable. - Branch name-drift NonDeterministicExecutionException message had expected/found inverted; now reports the checkpointed name as expected and the current registration as the drifted value.
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
… fix branch name-drift message Address Copilot review on #2553: - CreateParallel 'name' XML docs said a name change 'does not break replay', but the name is passed to ValidateReplayConsistency (throws on drift). Doc now states the name is part of the deterministic definition and must stay stable. - Branch name-drift NonDeterministicExecutionException message had expected/found inverted; now reports the checkpointed name as expected and the current registration as the drifted value.
075c4d8 to
8019228
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.