Skip to content

Add ParallelAsync for concurrent branch execution (DOTNET-8662)#2375

Draft
GarrettBeatty wants to merge 1 commit into
feature/durablefunctionfrom
gcbeatty/durable-parallel
Draft

Add ParallelAsync for concurrent branch execution (DOTNET-8662)#2375
GarrettBeatty wants to merge 1 commit into
feature/durablefunctionfrom
gcbeatty/durable-parallel

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Contributor

@GarrettBeatty GarrettBeatty commented May 14, 2026

#2216

What

Adds parallel branch execution to Amazon.Lambda.DurableExecution. ParallelAsync runs N branches concurrently with configurable concurrency limits and completion policies, returning an IBatchResult<T> with per-branch status and error information. The shared IBatchResult<T> family is reused by MapAsync in Wave 2.

Public API:

Type Purpose
IDurableContext.ParallelAsync<T>(Func[], ...) Run unnamed branches concurrently.
IDurableContext.ParallelAsync<T>(DurableBranch<T>[], ...) Same, but each branch carries an explicit name for traces / test inspection.
DurableBranch<T>(Name, Func) Named-branch record.
ParallelConfig MaxConcurrency, CompletionConfig, NestingType.
CompletionConfig When the batch is considered complete. Factories: AllSuccessful(), FirstSuccessful(), AllCompleted(). Validated MinSuccessful / ToleratedFailureCount / ToleratedFailurePercentage (0.0–1.0).
IBatchResult<T> Per-branch view: All / Succeeded / Failed / Started, GetResults, GetErrors, ThrowIfError, HasFailure, CompletionReason, count properties.
IBatchItem<T> Single-branch record (Index, Name, Status, Result, Error).
BatchItemStatus Succeeded / Failed / Started.
CompletionReason AllCompleted / MinSuccessfulReached / FailureToleranceExceeded.
NestingType Nested (default); Flat reserved for a follow-up PR (throws NotSupportedException today).
ParallelException Thrown when CompletionConfig signals FailureToleranceExceeded; carries the IBatchResult<T>.

Per-branch checkpoint payloads are serialized via the ILambdaSerializer registered on ILambdaContext.Serializer — same pattern as StepAsync / RunInChildContextAsync from #2370. There are no separate reflection / AOT-safe overload pairs: the AOT story is determined entirely by which serializer the user registers with the runtime (e.g., SourceGeneratorLambdaJsonSerializer<TContext>).

How

Internal/ParallelOperation<T> orchestrates branch dispatch on top of the existing child-context plumbing from #2370:

  • Branch dispatch. Each branch runs as a ChildContextOperation<T> with a deterministic ID via OperationIdGenerator.CreateChild, so concurrent branches get stable, namespaced IDs across replays. Optional MaxConcurrency is enforced with a SemaphoreSlim.
  • Completion semantics. The orchestrator monitors branch completion against CompletionConfig. FirstSuccessful short-circuits as soon as one branch succeeds (remaining branches surface in Started). MinSuccessful / ToleratedFailureCount / ToleratedFailurePercentage resolve to AllCompleted, MinSuccessfulReached, or FailureToleranceExceeded via CompletionReason.
  • Failure aggregation. Branch throws are captured as Failed items in IBatchResult<T>; the orchestrator only throws ParallelException when CompletionConfig signals FailureToleranceExceeded. Successful and tolerated-failure batches return normally so the user can inspect result.GetResults() / result.GetErrors().
  • Cancellation safety. ParallelOperation awaits Task.WhenAll(inFlight) before disposing the semaphore so a mid-dispatch cancellation or exception lets in-flight branches settle cleanly — no orphaned tasks holding semaphore permits.
  • Concurrency-safe state. ExecutionState is now thread-safe: a single lock guards reads/writes of _operations, _visitedOperations, and _isReplaying. Required so concurrent branches can replay without tearing the visited-set or seeing an inconsistent IsReplaying flag. Affects all operations — no behavioral regressions.
  • Replay. Each branch's ChildContextOperation replays from its own checkpoint independently; the parent Parallel operation reconstructs the IBatchResult<T> view from the branches' terminal states.
  • Reuses OperationSubTypes.Parallel and OperationSubTypes.ParallelBranch from Wave 0 (doc updates #2372).

Stacked on top of #2372.

Fixes DOTNET-8662.

Testing

31 new unit tests in ParallelOperationTests.cs and supporting fixtures:

  • CompletionConfig matrix: AllSuccessful, AllCompleted, FirstSuccessful, MinSuccessful, ToleratedFailureCount, ToleratedFailurePercentage — both pass and fail thresholds.
  • Concurrency: MaxConcurrency enforced via semaphore; unbounded when null; cancel-mid-dispatch leaves no orphan branches.
  • Concurrent ExecutionState access regression test (parallel writers do not corrupt the visited-set).
  • Replay determinism: mixed-status replay (SUCCEEDED + FAILED + STARTED), FirstSuccessful with all-fail, named vs. unnamed branches.
  • IBatchResult<T> accessors and GetResults / GetErrors / ThrowIfError semantics.
  • NestingType.Flat throws NotSupportedException (placeholder for follow-up).

6 new integration tests build successfully (require AWS credentials to run).

Build clean: 0 warnings, 0 errors on net8.0 and net10.0.

Out of scope (follow-up PRs)

  • MapAsync — same IBatchResult<T> machinery, but driven by an input collection.
  • NestingType.Flat — virtual contexts sharing a parent (~30% fewer checkpoint operations); placeholder today.
  • DurableLogger replay-suppression (currently NullLogger).
  • Annotations source-generator integration / [DurableExecution] attribute.
  • DurableTestRunner / Amazon.Lambda.DurableExecution.Testing package.
  • dotnet new lambda.DurableFunction blueprint.


COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]

COPY bin/publish/ ${LAMBDA_TASK_ROOT}

ENTRYPOINT ["/var/task/bootstrap"]
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-parallel branch from 19c0128 to fa13eef Compare May 14, 2026 21:49
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-wave0 branch from 464c591 to d308c3b Compare May 14, 2026 21:49
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-parallel branch from fa13eef to b7a06b4 Compare May 14, 2026 22:19
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-wave0 branch from d308c3b to be4c3ad Compare May 18, 2026 15:23
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-parallel branch from b7a06b4 to 08b2095 Compare May 18, 2026 15:44
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-wave0 branch 3 times, most recently from ad4d208 to 3acbed5 Compare May 20, 2026 17:46
Base automatically changed from gcbeatty/durable-wave0 to gcbeatty/durable-child-context May 20, 2026 17:46
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-child-context branch 2 times, most recently from 4d97473 to 8a6c41c Compare May 21, 2026 18:56
Base automatically changed from gcbeatty/durable-child-context to feature/durablefunction May 23, 2026 15:58
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-parallel branch from 08b2095 to 8664e8c Compare May 28, 2026 16:08
Adds parallel branch execution to the .NET Durable Execution SDK.
ParallelAsync runs N branches concurrently with configurable concurrency
limits and completion policies, returning an IBatchResult<T> with
per-branch status and error information.

Per-branch checkpoint payloads are serialized via the ILambdaSerializer
registered on ILambdaContext.Serializer (typically configured through
LambdaBootstrapBuilder.Create(handler, serializer)), matching the
StepAsync / RunInChildContextAsync pattern. There are no separate
reflection / AOT-safe overload pairs: the AOT story is determined
entirely by which serializer the user registers with the runtime.

Public surface:
- IDurableContext.ParallelAsync<T> (2 overloads: Func[] vs
  DurableBranch<T>[])
- DurableBranch<T> record (Name + Func)
- ParallelConfig (MaxConcurrency, CompletionConfig, NestingType)
- CompletionConfig with factories AllSuccessful() / FirstSuccessful() /
  AllCompleted(); ToleratedFailureCount / ToleratedFailurePercentage
  (validated 0.0-1.0)
- IBatchResult<T> with All / Succeeded / Failed / Started accessors,
  GetResults, GetErrors, ThrowIfError, HasFailure, CompletionReason,
  count properties
- IBatchItem<T> with Index, Name, Status, Result, Error
- BatchItemStatus { Succeeded, Failed, Started }
- CompletionReason { AllCompleted, MinSuccessfulReached,
  FailureToleranceExceeded }
- NestingType (Nested default; Flat throws NotSupportedException - reserved)
- ParallelException (carries IBatchResult; future-subclassable)

Internal:
- ParallelOperation<T> orchestrator dispatches branches with optional
  semaphore-bounded concurrency. Each branch runs as a
  ChildContextOperation<T> with deterministic ID via
  OperationIdGenerator.CreateChild.
- Branch failures aggregated as IBatchItem<T> entries; orchestrator
  throws ParallelException only when CompletionConfig signals
  FailureToleranceExceeded.
- Parent CONTEXT checkpoint records summary (CompletionReason +
  per-branch index/name/status); branch results live on per-branch
  CONTEXT checkpoints.
- ExecutionState now thread-safe (lock around reads/writes of
  _operations, _visitedOperations, _isReplaying). Required for
  concurrent branch replay; affects all operations but no regressions.
- ParallelOperation awaits Task.WhenAll(inFlight) before disposing
  the semaphore so cancellation/exception during dispatch lets
  in-flight branches settle cleanly.
- Reuses OperationSubTypes.Parallel / OperationSubTypes.ParallelBranch
  from Wave 0.

Adds 31 unit tests + 6 integration tests covering CompletionConfig
matrix, MaxConcurrency, FirstSuccessful short-circuit, replay
determinism, mixed-status replay, cancellation, and concurrency
stress.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@GarrettBeatty GarrettBeatty force-pushed the gcbeatty/durable-parallel branch from 8664e8c to e4da00c Compare May 28, 2026 18:47
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.

2 participants