Skip to content

feat(DurableExecution): incremental, heterogeneous Parallel API (#2519) - #2553

Draft
GarrettBeatty wants to merge 7 commits into
feature/per-step-serializerfrom
feature/heterogeneous-parallel
Draft

feat(DurableExecution): incremental, heterogeneous Parallel API (#2519)#2553
GarrettBeatty wants to merge 7 commits into
feature/per-step-serializerfrom
feature/heterogeneous-parallel

Conversation

@GarrettBeatty

Copy link
Copy Markdown
Contributor

Description

Implements #2519: an additive, branch-oriented parallel API for Amazon.Lambda.DurableExecution supporting heterogeneous per-branch result types and incremental branch registration, alongside the existing homogeneous ParallelAsync<T> overloads (which are unchanged).

await using var parallel = ctx.CreateParallel(name: "process-order");

IParallelBranch<InventoryReservation> inventory = parallel.BranchAsync(
    "inventory", async (branch, ct) => await ReserveInventoryAsync(branch, ct));
IParallelBranch<PaymentAuthorization> payment = parallel.BranchAsync(
    "payment", async (branch, ct) => await AuthorizePaymentAsync(branch, ct));

IBatchResult summary = await parallel.CompleteAsync();

InventoryReservation reserved = await inventory;   // own concrete type — no shared base, cast, or envelope
PaymentAuthorization  authed   = await payment;

What & why

Today every branch of a Parallel must share one generic result type T, forcing unrelated branch contracts into object, a common base type, or a wrapper. This adds a branch-scoped generic API (matching the Java SDK's ParallelDurableFuture) 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?)IDurableParallel
  • IDurableParallel : IAsyncDisposableBranchAsync<T>(name, func), CompleteAsync(ct)
  • IParallelBranch<T> — awaitable typed handle exposing Name / Index / Status

Design

  • Each branch runs as an existing ChildContextOperation<T> with the same deterministic child op id (hash("{parentId}-{index}")) and the same parent CONTEXT/Parallel BatchSummary checkpoint shape as batch Parallel — so replay, checkpoints, and reconstruction are identical and interoperable.
  • Branches start on registration, gated by a shared MaxConcurrency semaphore and a cooperative short-circuit token; CompleteAsync seals, awaits per CompletionConfig, and checkpoints the aggregate.
  • Deterministic replay: branch identity is positional (register the same branches in the same order); a name change at an index throws NonDeterministicExecutionException. Terminal-parent replay reconstructs from the frozen inline summary without re-running (re-running only overflow-stripped branches).
  • DisposeAsync auto-completes if CompleteAsync wasn't called, so await using always writes the terminal checkpoint.
  • MaxConcurrency, CompletionConfig, NestingType, cancellation, and the registered ILambdaSerializer are honored unchanged.
  • Refactors BatchSummary (de)serialization + overflow handling out of ConcurrentOperation<T> into a shared BatchSummaryCodec so the batch and incremental paths can't diverge on the wire format.

Testing

  • 16 unit tests (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.
  • 2 integration tests, both verified green against the durable-execution service:
    • 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: new "Incremental, heterogeneous branches" section in docs/core/parallel.md.

Note for reviewers

Because IParallelBranch<T> is awaitable, a bare parallel.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 + explicit GetResultAsync().

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

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.

🟡 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 after TrySetResult; the catch path then records a failed outcome but cannot replace the already-successful handle result, so summary.HasFailure/Status report failure while await branch returns 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. ParseSummary returns null for these payloads, but the operation remains in Terminal mode; every branch is then resolved as skipped and CompleteAsync synthesizes AllCompleted, 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.

Comment thread Libraries/src/Amazon.Lambda.DurableExecution/IDurableContext.cs
@GarrettBeatty
GarrettBeatty changed the base branch from master to feature/per-step-serializer-conformance September 2, 2026 21:09
@GarrettBeatty
GarrettBeatty force-pushed the feature/heterogeneous-parallel branch from 43ff3ef to 3fd7096 Compare September 2, 2026 21:10
@GarrettBeatty
GarrettBeatty requested a balanced review from Copilot September 2, 2026 21:11

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.

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);
Comment on lines +408 to +409
$"'{_name ?? _operationId}': expected name '{name}' but found '{summaryEntry.Name}' " +
$"from a previous invocation. Code must not change the order or name of branches " +
@GarrettBeatty
GarrettBeatty force-pushed the feature/per-step-serializer-conformance branch 2 times, most recently from cfac883 to 545df80 Compare September 3, 2026 03:23
@GarrettBeatty
GarrettBeatty force-pushed the feature/heterogeneous-parallel branch from 3fd7096 to 9b1220e Compare September 3, 2026 03:35
GarrettBeatty added a commit that referenced this pull request Sep 3, 2026
…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.
GarrettBeatty and others added 7 commits September 3, 2026 03:48
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
@GarrettBeatty
GarrettBeatty force-pushed the feature/heterogeneous-parallel branch from 9b727b7 to c760c01 Compare September 3, 2026 03:51
@GarrettBeatty
GarrettBeatty changed the base branch from feature/per-step-serializer-conformance to feature/per-step-serializer September 3, 2026 03:51
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.

3 participants