Skip to content

Align durable Parallel/Map failure semantics with the JS SDK#2465

Merged
GarrettBeatty merged 1 commit into
devfrom
durable-concurrent-js-parity
Jul 8, 2026
Merged

Align durable Parallel/Map failure semantics with the JS SDK#2465
GarrettBeatty merged 1 commit into
devfrom
durable-concurrent-js-parity

Conversation

@GarrettBeatty

@GarrettBeatty GarrettBeatty commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings ParallelAsync/MapAsync in Amazon.Lambda.DurableExecution to full parity with the JS reference SDK's concurrent-operation failure semantics. Verified against all four SDK clones (JS/Python/Java) and the canonical aws-durable-execution-docs spec.

Preview (0.x) — intentionally backward-incompatible.

Changes

  1. No auto-throw. ParallelAsync/MapAsync now always return IBatchResult. Failure surfaces via CompletionReason / HasFailure / ThrowIfError() — matching JS, Python, and Java. Previously the failure-tolerance path threw.
  2. Removed ParallelException and MapException. No SDK has a batch-level exception; ThrowIfError() throws the individual child error.
  3. Empty CompletionConfig() is now fail-fast (any failure → FailureToleranceExceeded), matching JS/Python getCompletionReason. AllCompleted() is redefined to ToleratedFailureCount = int.MaxValue so it stays lenient after the flip. Map's default completion flipped from AllCompleted() to AllSuccessful(), matching Parallel and JS/Python.
  4. MapConfig is now generic (MapConfig<TItem>) so ItemNamer is typed Func<TItem, int, string> instead of Func<object, int, string>.

Also corrects a stale doc statement that NestingType.Flat throws NotSupportedException (Flat is implemented).

New user experience (before → after)

Handling failures — no more try/catch on the batch operation

Before, a failure that exceeded the tolerance threw a batch-level exception, so you had two different error channels to reason about (the thrown ParallelException/MapException and the per-item Failed list). Now the operation never throws — you always get an IBatchResult back and inspect it.

// BEFORE — batch throws when the completion policy is violated
try
{
    var batch = await ctx.ParallelAsync(branches, name: "fan-out");
    var results = batch.GetResults();
}
catch (ParallelException ex)   // <- removed type
{
    // handle failure
}

// AFTER — always returns; inspect the result
var batch = await ctx.ParallelAsync(branches, name: "fan-out");
if (batch.HasFailure)
{
    // batch.CompletionReason == CompletionReason.FailureToleranceExceeded
    // batch.Failed has the per-branch errors
}
var results = batch.GetResults();

// ...or opt into strict throw-on-failure explicitly:
batch.ThrowIfError();          // throws the individual child error, not a wrapper

MapAsync now defaults to fail-fast (like ParallelAsync and JS/Python)

Previously MapAsync ran every item regardless of failures by default. Now any item failure resolves the map with FailureToleranceExceeded. To keep the old run-everything behavior, opt in with AllCompleted().

// BEFORE — ran all items, failures only visible in result.Failed
var result = await ctx.MapAsync(items, ProcessItemAsync, name: "process");

// AFTER — same call is now fail-fast on the first failing item.
// To preserve the old "run everything" behavior:
var result = await ctx.MapAsync(items, ProcessItemAsync, name: "process",
    config: new MapConfig<Order> { CompletionConfig = CompletionConfig.AllCompleted() });

MapConfig is now generic — ItemNamer is strongly typed

// BEFORE — item comes through as object, requires a cast
var cfg = new MapConfig
{
    ItemNamer = (item, i) => $"order-{((Order)item).Id}",
};

// AFTER — item is TItem
var cfg = new MapConfig<Order>
{
    ItemNamer = (order, i) => $"order-{order.Id}",
};

An empty/default CompletionConfig() is now fail-fast

// A bare CompletionConfig() (or omitting config entirely) == AllSuccessful() == fail-fast.
// Leniency must now be opted into explicitly:
new CompletionConfig();                 // fail-fast (any failure → FailureToleranceExceeded)
CompletionConfig.AllSuccessful();       // fail-fast (same thing)
CompletionConfig.AllCompleted();        // lenient — run everything (ToleratedFailureCount = int.MaxValue)

Migration checklist

  • Remove catch (ParallelException) / catch (MapException) — replace with if (batch.HasFailure) or batch.ThrowIfError().
  • new MapConfig { ... }new MapConfig<TItem> { ... }; drop the cast in ItemNamer.
  • If you relied on MapAsync running every item, set CompletionConfig = CompletionConfig.AllCompleted().
  • Audit any bare new CompletionConfig() — it is now fail-fast, not lenient.

Cross-SDK context

Note on terminology. "Fail-fast" and .NET's AllSuccessful() describe the same behavior — any single branch failure resolves the batch with FailureToleranceExceeded and no further branches are dispatched. They are just different labels: JS has no named preset (its default is the absence of a config, which getCompletionReason treats as fail-fast), whereas .NET exposes a factory method (AllSuccessful(), i.e. ToleratedFailureCount = 0) that produces the identical rule (failed > 0 → fail). An empty new CompletionConfig() in .NET is equivalent to AllSuccessful(). So when the table below reads AllSuccessful() for .NET, that is the fail-fast default. (Fail-fast stops dispatching remaining branches; it does not cancel branches already in flight.)

The table lists each SDK's default in that SDK's own vocabulary, with the resulting behavior in parentheses:

SDK Parallel default Map default Auto-throws?
JS no config (fail-fast) no config (fail-fast) no
Python all_successful (fail-fast) empty CompletionConfig (fail-fast) no
Java allCompleted() (lenient) allCompleted() (lenient) no (future/.get() summary API)
.NET (this PR) AllSuccessful() (fail-fast) AllSuccessful() (fail-fast) no

.NET's fail-fast default matches JS on both operations and matches Python on Map (Python's Parallel default, all_successful, is also fail-fast, so the behavior lines up there too — only the spelling differs). Java remains an outlier by its own design: both its defaults are lenient (allCompleted()), and it has no ItemNamer.

Testing

  • Library + all test projects build clean (TreatWarningsAsErrors on, 0 warnings).
  • 405 unit tests and 153 testing-shared tests pass, including both failure-tolerance scenarios exercised end-to-end through the in-memory runner.
  • Integration tests (live-AWS) were rewritten for the new contract and compile; not run here.

Change file

.autover/changes/durable-concurrent-js-parity.json (Minor, preview).

Full JS-SDK parity for ParallelAsync/MapAsync (preview 0.x, breaking):

- No auto-throw: both operations always return IBatchResult; failure
  surfaces via CompletionReason/HasFailure/ThrowIfError(), matching
  JS/Python/Java.
- Removed ParallelException and MapException.
- Empty CompletionConfig() is now fail-fast (any failure resolves
  FailureToleranceExceeded), matching JS/Python getCompletionReason.
  AllCompleted() redefined to ToleratedFailureCount = int.MaxValue so it
  stays lenient. Map default flipped from AllCompleted() to
  AllSuccessful().
- MapConfig is now generic (MapConfig<TItem>) so ItemNamer is typed
  Func<TItem, int, string> instead of Func<object, int, string>.

Updates all consumers (context, config, ops, docs, analyzer stubs,
shared workflows/scenarios, unit + integration tests). Also corrects a
stale docs claim that NestingType.Flat throws NotSupportedException.

405 unit tests and 153 testing-shared tests pass.
@GarrettBeatty GarrettBeatty marked this pull request as ready for review July 8, 2026 14:22
@GarrettBeatty GarrettBeatty requested review from a team as code owners July 8, 2026 14:22
@GarrettBeatty GarrettBeatty requested review from normj and philasmar July 8, 2026 14:22
@GarrettBeatty GarrettBeatty merged commit 83e48e4 into dev Jul 8, 2026
5 of 6 checks passed
@GarrettBeatty GarrettBeatty deleted the durable-concurrent-js-parity branch July 8, 2026 17:59
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