Skip to content

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

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

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

Conversation

@GarrettBeatty
Copy link
Copy Markdown
Contributor

@GarrettBeatty GarrettBeatty commented May 14, 2026

#2216

Summary

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 (e.g., SourceGeneratorLambdaJsonSerializer<TContext> for AOT scenarios).

Stacked on top of #2372 (Wave 0 cross-cutting types).

Fixes DOTNET-8662.

The shared IBatchResult<T> family added here will be reused by MapAsync (Wave 2).

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 for a follow-up)
  • ParallelException (carries IBatchResult; future-subclassable)

Internal

  • ParallelOperation<T> orchestrator dispatches branches with optional semaphore-bounded concurrency. Each branch runs as a ChildContextOperation<T> with a deterministic ID via OperationIdGenerator.CreateChild.
  • Branch failures aggregated as IBatchItem<T> entries; orchestrator throws ParallelException only when CompletionConfig signals FailureToleranceExceeded.
  • 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.

Test plan

  • Build clean (zero warnings, TreatWarningsAsErrors enforced) on net8.0 and net10.0
  • 31 new unit tests pass alongside existing 161, for 192 total, including:
    • CompletionConfig matrix (AllSuccessful, AllCompleted, FirstSuccessful, MinSuccessful, ToleratedFailureCount, ToleratedFailurePercentage)
    • Cancel-mid-dispatch regression test (no orphan branches)
    • Concurrent ExecutionState access regression test
    • Replay determinism, mixed-status replay, FirstSuccessful all-fail
  • 6 new integration tests build successfully (require AWS credentials to run)

Generated with Claude Code


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
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 b7a06b4 to 08b2095 Compare May 18, 2026 15:44
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