Skip to content

Commit 83e48e4

Browse files
Align durable Parallel/Map failure semantics with the JS SDK (#2465)
1 parent 3014f16 commit 83e48e4

24 files changed

Lines changed: 382 additions & 422 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"Projects": [
3+
{
4+
"Name": "Amazon.Lambda.DurableExecution",
5+
"Type": "Minor",
6+
"ChangelogMessages": [
7+
"Align ParallelAsync/MapAsync failure semantics with the JS/Python SDKs (breaking, preview). ParallelAsync and MapAsync no longer throw on failure — they always return an IBatchResult; inspect CompletionReason/HasFailure or call ThrowIfError(). The ParallelException and MapException types are removed. MapConfig.CompletionConfig now defaults to AllSuccessful() (fail-fast), matching ParallelConfig. An empty CompletionConfig() is now fail-fast (any failure resolves FailureToleranceExceeded); use CompletionConfig.AllCompleted() to run every unit regardless of failures. MapConfig is now generic (MapConfig<TItem>) so ItemNamer is typed Func<TItem, int, string> instead of Func<object, int, string>. Preview."
8+
]
9+
}
10+
]
11+
}

Libraries/src/Amazon.Lambda.DurableExecution/CompletionConfig.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,29 @@ public double? ToleratedFailurePercentage
9090
}
9191

9292
/// <summary>
93-
/// All items must succeed. Equivalent to
94-
/// <see cref="ToleratedFailureCount"/> = 0. The default for
95-
/// <see cref="ParallelConfig.CompletionConfig"/>.
93+
/// All items must succeed — any single failure resolves the batch with
94+
/// <see cref="CompletionReason.FailureToleranceExceeded"/>. Equivalent to
95+
/// <see cref="ToleratedFailureCount"/> = 0, and to a default (empty)
96+
/// <see cref="CompletionConfig"/>. The default for both
97+
/// <see cref="ParallelConfig.CompletionConfig"/> and
98+
/// <see cref="MapConfig{TItem}.CompletionConfig"/>, matching the JS/Python SDKs.
9699
/// </summary>
97100
public static CompletionConfig AllSuccessful() => new() { ToleratedFailureCount = 0 };
98101

99102
/// <summary>
100103
/// Run every branch regardless of failures; surface failures per-item via
101-
/// <see cref="IBatchResult{T}.Failed"/>. Resolution does not auto-throw —
102-
/// the caller can inspect the result and call
103-
/// <see cref="IBatchResult{T}.ThrowIfError"/> if they want strict-success
104-
/// behavior.
104+
/// <see cref="IBatchResult{T}.Failed"/>. Never resolves with
105+
/// <see cref="CompletionReason.FailureToleranceExceeded"/>. The caller
106+
/// inspects the result and can call <see cref="IBatchResult{T}.ThrowIfError"/>
107+
/// if it wants strict-success behavior.
105108
/// </summary>
106-
public static CompletionConfig AllCompleted() => new();
109+
/// <remarks>
110+
/// Sets <see cref="ToleratedFailureCount"/> to <see cref="int.MaxValue"/> so it
111+
/// stays lenient. An <em>empty</em> <see cref="CompletionConfig"/> is fail-fast
112+
/// (equivalent to <see cref="AllSuccessful"/>), so leniency must be opted into
113+
/// explicitly.
114+
/// </remarks>
115+
public static CompletionConfig AllCompleted() => new() { ToleratedFailureCount = int.MaxValue };
107116

108117
/// <summary>
109118
/// Resolve once at least one branch has succeeded. Branches that were not

Libraries/src/Amazon.Lambda.DurableExecution/CompletionReason.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ public enum CompletionReason
2424

2525
/// <summary>
2626
/// <see cref="CompletionConfig.ToleratedFailureCount"/> or
27-
/// <see cref="CompletionConfig.ToleratedFailurePercentage"/> was exceeded.
28-
/// The batch is considered failed and surfaces a
29-
/// <see cref="ParallelException"/> when awaited.
27+
/// <see cref="CompletionConfig.ToleratedFailurePercentage"/> was exceeded (a
28+
/// default/empty <see cref="CompletionConfig"/> is fail-fast, so any failure
29+
/// trips this). The batch is considered failed: <see cref="IBatchResult.HasFailure"/>
30+
/// is <c>true</c> and <see cref="IBatchResult{T}.ThrowIfError"/> surfaces the
31+
/// first item error. The operation itself does not throw.
3032
/// </summary>
3133
FailureToleranceExceeded
3234
}

Libraries/src/Amazon.Lambda.DurableExecution/DurableContext.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,21 +259,21 @@ public Task<IBatchResult<TResult>> MapAsync<TItem, TResult>(
259259
IReadOnlyList<TItem> items,
260260
Func<IDurableContext, TItem, int, IReadOnlyList<TItem>, CancellationToken, Task<TResult>> func,
261261
string? name = null,
262-
MapConfig? config = null,
262+
MapConfig<TItem>? config = null,
263263
CancellationToken cancellationToken = default)
264264
=> RunMap(items, func, name, config, cancellationToken);
265265

266266
private Task<IBatchResult<TResult>> RunMap<TItem, TResult>(
267267
IReadOnlyList<TItem> items,
268268
Func<IDurableContext, TItem, int, IReadOnlyList<TItem>, CancellationToken, Task<TResult>> func,
269269
string? name,
270-
MapConfig? config,
270+
MapConfig<TItem>? config,
271271
CancellationToken cancellationToken)
272272
{
273273
if (items == null) throw new ArgumentNullException(nameof(items));
274274
if (func == null) throw new ArgumentNullException(nameof(func));
275275

276-
var effectiveConfig = config ?? new MapConfig();
276+
var effectiveConfig = config ?? new MapConfig<TItem>();
277277

278278
var serializer = LambdaSerializerHelper.GetRequired(LambdaContext);
279279

Libraries/src/Amazon.Lambda.DurableExecution/DurableExecutionException.cs

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -98,69 +98,3 @@ public ChildContextException(string message) : base(message) { }
9898
/// <summary>Creates a <see cref="ChildContextException"/> wrapping an inner exception.</summary>
9999
public ChildContextException(string message, Exception innerException) : base(message, innerException) { }
100100
}
101-
102-
/// <summary>
103-
/// Thrown when a parallel operation resolves with
104-
/// <see cref="CompletionReason.FailureToleranceExceeded"/>. The aggregate
105-
/// <see cref="IBatchResult"/> is preserved on <see cref="Result"/> so callers
106-
/// can inspect per-branch outcomes.
107-
/// </summary>
108-
/// <remarks>
109-
/// This is the base type for parallel failures. Subclasses may be added in
110-
/// future releases (for example, a dedicated
111-
/// <c>ParallelFailureToleranceExceededException</c>); catching
112-
/// <see cref="ParallelException"/> remains forward-compatible.
113-
/// </remarks>
114-
public class ParallelException : DurableExecutionException
115-
{
116-
/// <summary>
117-
/// The aggregate result of the parallel operation. Type-erased — cast to
118-
/// <c>IBatchResult&lt;T&gt;</c> if the per-branch result type is known.
119-
/// </summary>
120-
public IBatchResult? Result { get; init; }
121-
122-
/// <summary>
123-
/// Why the parallel operation resolved.
124-
/// </summary>
125-
public CompletionReason CompletionReason { get; init; }
126-
127-
/// <summary>Creates an empty <see cref="ParallelException"/>.</summary>
128-
public ParallelException() { }
129-
/// <summary>Creates a <see cref="ParallelException"/> with the given message.</summary>
130-
public ParallelException(string message) : base(message) { }
131-
/// <summary>Creates a <see cref="ParallelException"/> wrapping an inner exception.</summary>
132-
public ParallelException(string message, Exception innerException) : base(message, innerException) { }
133-
}
134-
135-
/// <summary>
136-
/// Thrown when a map operation resolves with
137-
/// <see cref="CompletionReason.FailureToleranceExceeded"/>. The aggregate
138-
/// <see cref="IBatchResult"/> is preserved on <see cref="Result"/> so callers
139-
/// can inspect per-item outcomes.
140-
/// </summary>
141-
/// <remarks>
142-
/// This is the base type for map failures. Subclasses may be added in future
143-
/// releases; catching <see cref="MapException"/> remains forward-compatible.
144-
/// A dedicated type (rather than reusing <see cref="ParallelException"/>) lets
145-
/// callers pattern-match which concurrent operation failed.
146-
/// </remarks>
147-
public class MapException : DurableExecutionException
148-
{
149-
/// <summary>
150-
/// The aggregate result of the map operation. Type-erased — cast to
151-
/// <c>IBatchResult&lt;T&gt;</c> if the per-item result type is known.
152-
/// </summary>
153-
public IBatchResult? Result { get; init; }
154-
155-
/// <summary>
156-
/// Why the map operation resolved.
157-
/// </summary>
158-
public CompletionReason CompletionReason { get; init; }
159-
160-
/// <summary>Creates an empty <see cref="MapException"/>.</summary>
161-
public MapException() { }
162-
/// <summary>Creates a <see cref="MapException"/> with the given message.</summary>
163-
public MapException(string message) : base(message) { }
164-
/// <summary>Creates a <see cref="MapException"/> wrapping an inner exception.</summary>
165-
public MapException(string message, Exception innerException) : base(message, innerException) { }
166-
}

Libraries/src/Amazon.Lambda.DurableExecution/IBatchResult.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
namespace Amazon.Lambda.DurableExecution;
55

66
/// <summary>
7-
/// Non-generic marker for <see cref="IBatchResult{T}"/>. Used by
8-
/// <see cref="ParallelException.Result"/> so callers can hold a reference to
9-
/// the aggregate result without knowing the per-branch type at compile time.
7+
/// Non-generic marker for <see cref="IBatchResult{T}"/>. Lets callers hold a
8+
/// reference to the aggregate result, or read the completion bookkeeping, without
9+
/// knowing the per-branch type at compile time.
1010
/// </summary>
1111
public interface IBatchResult
1212
{

Libraries/src/Amazon.Lambda.DurableExecution/IDurableContext.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -377,9 +377,11 @@ Task<TState> WaitForConditionAsync<TState>(
377377
/// <remarks>
378378
/// On per-branch failure (a branch's user function throws), the failure is
379379
/// captured on the corresponding <see cref="IBatchItem{T}"/> instead of
380-
/// aborting the parallel. The parallel only throws
381-
/// <see cref="ParallelException"/> when <see cref="CompletionConfig"/>
382-
/// criteria are violated. Use
380+
/// aborting the parallel. The parallel NEVER throws on failure — it always
381+
/// returns an <see cref="IBatchResult{T}"/>. By default
382+
/// (<see cref="CompletionConfig.AllSuccessful"/>, fail-fast) any branch failure
383+
/// resolves it with <see cref="CompletionReason.FailureToleranceExceeded"/>;
384+
/// failures surface via <see cref="IBatchResult.HasFailure"/>. Use
383385
/// <see cref="IBatchResult{T}.ThrowIfError"/> for explicit strict-success
384386
/// semantics. Per-branch results are serialized to checkpoints using the
385387
/// <see cref="ILambdaSerializer"/> registered on
@@ -449,22 +451,26 @@ Task<IBatchResult<T>> ParallelAsync<T>(
449451
/// Process a collection of items concurrently, running <paramref name="func"/>
450452
/// once per item. Each item runs inside its own child context; per-item
451453
/// results are aggregated into an <see cref="IBatchResult{TResult}"/>. Items
452-
/// are dispatched up to <see cref="MapConfig.MaxConcurrency"/>; the aggregate
453-
/// resolves according to <see cref="MapConfig.CompletionConfig"/>.
454+
/// are dispatched up to <see cref="MapConfig{TItem}.MaxConcurrency"/>; the aggregate
455+
/// resolves according to <see cref="MapConfig{TItem}.CompletionConfig"/>.
454456
/// </summary>
455457
/// <remarks>
456458
/// The per-item function receives the durable context, the item, its
457459
/// zero-based index, the full source list, and a
458460
/// <see cref="CancellationToken"/> linking the
459461
/// caller-supplied token with the SDK's workflow-shutdown signal (it is also
460462
/// tripped cooperatively when a sibling item satisfies the
461-
/// <see cref="MapConfig.CompletionConfig"/> and the map short-circuits). On
463+
/// <see cref="MapConfig{TItem}.CompletionConfig"/> and the map short-circuits). On
462464
/// per-item failure (the user function throws), the failure is captured on
463465
/// the corresponding <see cref="IBatchItem{TResult}"/> instead of aborting
464-
/// the map. By default (<see cref="CompletionConfig.AllCompleted"/>) every
465-
/// item runs and failures surface via <see cref="IBatchResult{TResult}.Failed"/>;
466-
/// the map throws <see cref="MapException"/> only when
467-
/// <see cref="CompletionConfig"/> criteria are violated. Use
466+
/// the map. The map NEVER throws on failure — it always returns an
467+
/// <see cref="IBatchResult{TResult}"/>. By default
468+
/// (<see cref="CompletionConfig.AllSuccessful"/>, fail-fast) any item failure
469+
/// resolves the map with
470+
/// <see cref="CompletionReason.FailureToleranceExceeded"/>; use
471+
/// <see cref="CompletionConfig.AllCompleted"/> to run every item regardless.
472+
/// Failures surface via <see cref="IBatchResult{TResult}.Failed"/> /
473+
/// <see cref="IBatchResult.HasFailure"/>; call
468474
/// <see cref="IBatchResult{TResult}.ThrowIfError"/> for explicit
469475
/// strict-success semantics. Per-item results are serialized to checkpoints
470476
/// using the <see cref="ILambdaSerializer"/> registered on
@@ -474,7 +480,7 @@ Task<IBatchResult<TResult>> MapAsync<TItem, TResult>(
474480
IReadOnlyList<TItem> items,
475481
Func<IDurableContext, TItem, int, IReadOnlyList<TItem>, CancellationToken, Task<TResult>> func,
476482
string? name = null,
477-
MapConfig? config = null,
483+
MapConfig<TItem>? config = null,
478484
CancellationToken cancellationToken = default);
479485
}
480486

Libraries/src/Amazon.Lambda.DurableExecution/Internal/CompletionPolicy.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,20 @@ internal readonly struct CompletionPolicy
2222
private readonly int? _toleratedFailureCount;
2323
private readonly double? _toleratedFailurePercentage;
2424

25+
// True when NO completion criteria are set at all (every field null). This is
26+
// the fail-fast default, matching the JS/Python SDKs: an empty CompletionConfig
27+
// fails the batch on the first failed unit. Setting ANY field (even
28+
// MinSuccessful alone) opts out of this and uses the explicit thresholds below.
29+
private readonly bool _failFastOnAnyFailure;
30+
2531
public CompletionPolicy(CompletionConfig config)
2632
{
2733
_minSuccessful = config.MinSuccessful;
2834
_toleratedFailureCount = config.ToleratedFailureCount;
2935
_toleratedFailurePercentage = config.ToleratedFailurePercentage;
36+
_failFastOnAnyFailure = _minSuccessful is null
37+
&& _toleratedFailureCount is null
38+
&& _toleratedFailurePercentage is null;
3039
}
3140

3241
/// <summary>
@@ -61,12 +70,18 @@ public CompletionReason Evaluate(int succeeded, int failed, int started, int tot
6170
private bool MinSuccessfulReached(int succeeded)
6271
=> _minSuccessful is { } min && succeeded >= min;
6372

64-
// Failure count or ratio STRICTLY exceeds a configured threshold. Only a
65-
// threshold that was explicitly set can trip this — an "empty" CompletionConfig
66-
// (all properties null) is permissive. CompletionConfig.AllSuccessful() opts
67-
// into fail-fast by setting ToleratedFailureCount = 0.
73+
// Failure count or ratio STRICTLY exceeds a configured threshold. An "empty"
74+
// CompletionConfig (all properties null) is FAIL-FAST — any failure trips it —
75+
// matching the JS/Python SDKs. Setting any explicit threshold opts out of that
76+
// and uses the thresholds below. CompletionConfig.AllSuccessful() (which sets
77+
// ToleratedFailureCount = 0) and the empty config are therefore equivalent;
78+
// CompletionConfig.AllCompleted() sets ToleratedFailureCount = int.MaxValue to
79+
// stay lenient.
6880
private bool FailureToleranceExceeded(int failed, int totalBranches)
6981
{
82+
if (_failFastOnAnyFailure)
83+
return failed > 0;
84+
7085
if (_toleratedFailureCount is { } tfc && failed > tfc)
7186
return true;
7287

0 commit comments

Comments
 (0)