Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 47 additions & 10 deletions Docs/durable-execution-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1108,18 +1108,22 @@ public interface IDurableContext
CancellationToken cancellationToken = default);

/// <summary>
/// Create a callback for external system integration.
/// Create a callback for external system integration. The result is
/// deserialized using the <see cref="ILambdaSerializer"/> registered on
/// <see cref="ILambdaContext.Serializer"/> (typically configured via
/// <c>LambdaBootstrapBuilder.Create(handler, serializer)</c>).
/// </summary>
Task<ICallback<T>> CreateCallbackAsync<T>(
string? name = null,
CallbackConfig? config = null,
CancellationToken cancellationToken = default);

/// <summary>
/// Wait for an external system to respond via callback.
/// Wait for an external system to respond via callback. Composes
/// CreateCallback + Step(submitter) + GetResult inside a child context.
/// </summary>
Task<T> WaitForCallbackAsync<T>(
Func<string, ICallbackContext, Task> submitter,
Func<string, IWaitForCallbackContext, Task> submitter,
string? name = null,
WaitForCallbackConfig? config = null,
CancellationToken cancellationToken = default);
Expand Down Expand Up @@ -1208,6 +1212,20 @@ public interface IStepContext
string OperationId { get; }
}

/// <summary>
/// Context passed to the submitter delegate of <c>WaitForCallbackAsync</c>.
/// Distinct from <see cref="IStepContext"/> so the submitter API can evolve
/// independently. Mirrors <c>WaitForCallbackContext</c> in the Python and
/// JavaScript SDKs (logger-only surface).
/// </summary>
public interface IWaitForCallbackContext
{
/// <summary>
/// Logger scoped to the submitter step (replay-safe).
/// </summary>
ILogger Logger { get; }
}

/// <summary>
/// A named branch for parallel execution. Named branches appear in execution
/// traces and can be inspected by name in the test runner.
Expand Down Expand Up @@ -1279,9 +1297,9 @@ public class CallbackConfig
/// </summary>
public TimeSpan HeartbeatTimeout { get; set; } = TimeSpan.Zero;

// Note: there is no Serializer property here. Custom serializers are
// supplied via the AOT-safe CreateCallbackAsync(..., ICheckpointSerializer<T>, ...)
// overload, matching the pattern established by StepAsync.
// Note: there is no Serializer property here. The result is serialized via
// the ILambdaSerializer registered on ILambdaContext.Serializer (typically
// configured by LambdaBootstrapBuilder.Create(handler, serializer)).
}

/// <summary>
Expand Down Expand Up @@ -1568,7 +1586,9 @@ public interface ICallback<T>
/// Wait for and return the callback result.
/// Suspends execution until the result is available.
/// </summary>
Task<T?> GetResultAsync(CancellationToken cancellationToken = default);
/// <exception cref="CallbackFailedException">External system reported failure.</exception>
/// <exception cref="CallbackTimeoutException">Service marked the callback TIMED_OUT.</exception>
Task<T> GetResultAsync(CancellationToken cancellationToken = default);
}

/// <summary>
Expand Down Expand Up @@ -1605,14 +1625,31 @@ public class StepException : DurableExecutionException
}

/// <summary>
/// Thrown when a callback fails or times out.
/// Base exception for callback failures. Concrete subclasses distinguish
/// failure modes — pattern-match the subclass type rather than inspecting
/// a flag.
/// </summary>
public class CallbackException : DurableExecutionException
{
public string? CallbackId { get; }
public bool IsTimeout { get; }
public string? CallbackId { get; init; }
public string? ErrorType { get; init; }
public string? ErrorData { get; init; }
public IReadOnlyList<string>? OriginalStackTrace { get; init; }
}

/// <summary>External system reported a failure result for the callback.</summary>
public class CallbackFailedException : CallbackException { }

/// <summary>Service marked the callback TIMED_OUT (overall or heartbeat).</summary>
public class CallbackTimeoutException : CallbackException { }

/// <summary>
/// Submitter step (the inner step inside <c>WaitForCallbackAsync</c>) failed
/// after retries are exhausted. Wraps the underlying <c>StepException</c>.
/// Only thrown from <c>WaitForCallbackAsync</c>.
/// </summary>
public class CallbackSubmitterException : CallbackException { }

/// <summary>
/// Thrown when an invoked function fails.
/// </summary>
Expand Down
77 changes: 77 additions & 0 deletions Libraries/src/Amazon.Lambda.DurableExecution/CallbackConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
namespace Amazon.Lambda.DurableExecution;

/// <summary>
/// Configuration for callback operations created via
/// <see cref="IDurableContext.CreateCallbackAsync{T}(string?, CallbackConfig?, System.Threading.CancellationToken)"/>.
/// </summary>
public class CallbackConfig
{
private TimeSpan _timeout = TimeSpan.Zero;
private TimeSpan _heartbeatTimeout = TimeSpan.Zero;

/// <summary>
/// Maximum total time the service will wait for the external system to
/// complete the callback. <see cref="TimeSpan.Zero"/> (default) means no
/// overall timeout — only <see cref="HeartbeatTimeout"/> applies (if set).
/// </summary>
/// <remarks>
/// The service's timer granularity is 1 second, so values strictly between
/// <see cref="TimeSpan.Zero"/> and 1 second are rejected to avoid silent
/// rounding. Use <see cref="TimeSpan.Zero"/> to disable the timeout, or a
/// value of at least 1 second.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when set to a positive value less than 1 second.
/// </exception>
public TimeSpan Timeout
{
get => _timeout;
set
{
ValidateTimeout(value, nameof(Timeout));
_timeout = value;
}
}

/// <summary>
/// Maximum gap between heartbeat signals from the external system before
/// the service marks the callback as timed-out.
/// <see cref="TimeSpan.Zero"/> (default) means no heartbeat timeout.
/// </summary>
/// <remarks>
/// The service's timer granularity is 1 second, so values strictly between
/// <see cref="TimeSpan.Zero"/> and 1 second are rejected to avoid silent
/// rounding. Use <see cref="TimeSpan.Zero"/> to disable the heartbeat
/// timeout, or a value of at least 1 second.
/// </remarks>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when set to a positive value less than 1 second.
/// </exception>
public TimeSpan HeartbeatTimeout
{
get => _heartbeatTimeout;
set
{
ValidateTimeout(value, nameof(HeartbeatTimeout));
_heartbeatTimeout = value;
}
}

private static void ValidateTimeout(TimeSpan value, string paramName)
{
// Allow Zero (means "not set"); reject negative; reject sub-second
// positive values to mirror WaitAsync's behavior and prevent silent
// rounding-up inside BuildCallbackOptions.
if (value < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
paramName, value, $"{paramName} must be non-negative.");
}
if (value > TimeSpan.Zero && value < TimeSpan.FromSeconds(1))
{
throw new ArgumentOutOfRangeException(
paramName, value,
$"{paramName} must be at least 1 second (or TimeSpan.Zero to disable).");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace Amazon.Lambda.DurableExecution;

/// <summary>
/// Base exception type for callback failures surfaced from
/// <see cref="ICallback{T}.GetResultAsync(System.Threading.CancellationToken)"/>
/// or
/// <see cref="IDurableContext.WaitForCallbackAsync{T}(System.Func{string, IWaitForCallbackContext, System.Threading.Tasks.Task}, string?, WaitForCallbackConfig?, System.Threading.CancellationToken)"/>.
/// Concrete subclasses distinguish failure modes — pattern-match
/// <see cref="CallbackFailedException"/>, <see cref="CallbackTimeoutException"/>,
/// or <see cref="CallbackSubmitterException"/> in <c>catch</c> clauses.
/// </summary>
public class CallbackException : DurableExecutionException
{
/// <summary>The callback ID associated with the failure (if known).</summary>
public string? CallbackId { get; init; }

/// <summary>The fully-qualified type name of the original error, if known.</summary>
public string? ErrorType { get; init; }

/// <summary>Optional structured error data attached by the external system.</summary>
public string? ErrorData { get; init; }

/// <summary>Stack trace of the original error, captured before serialization.</summary>
public IReadOnlyList<string>? OriginalStackTrace { get; init; }

/// <summary>Creates an empty <see cref="CallbackException"/>.</summary>
public CallbackException() { }

/// <summary>Creates a <see cref="CallbackException"/> with the given message.</summary>
public CallbackException(string message) : base(message) { }

/// <summary>Creates a <see cref="CallbackException"/> wrapping an inner exception.</summary>
public CallbackException(string message, Exception innerException) : base(message, innerException) { }
}

/// <summary>
/// Thrown when the external system reports a failure result for a callback
/// (via <c>SendDurableExecutionCallbackFailure</c>).
/// </summary>
public class CallbackFailedException : CallbackException
{
/// <summary>Creates an empty <see cref="CallbackFailedException"/>.</summary>
public CallbackFailedException() { }

/// <summary>Creates a <see cref="CallbackFailedException"/> with the given message.</summary>
public CallbackFailedException(string message) : base(message) { }

/// <summary>Creates a <see cref="CallbackFailedException"/> wrapping an inner exception.</summary>
public CallbackFailedException(string message, Exception innerException) : base(message, innerException) { }
}

/// <summary>
/// Thrown when the durable execution service marks a callback as timed-out —
/// either the overall <see cref="CallbackConfig.Timeout"/> or the
/// <see cref="CallbackConfig.HeartbeatTimeout"/> elapsed.
/// </summary>
public class CallbackTimeoutException : CallbackException
{
/// <summary>Creates an empty <see cref="CallbackTimeoutException"/>.</summary>
public CallbackTimeoutException() { }

/// <summary>Creates a <see cref="CallbackTimeoutException"/> with the given message.</summary>
public CallbackTimeoutException(string message) : base(message) { }

/// <summary>Creates a <see cref="CallbackTimeoutException"/> wrapping an inner exception.</summary>
public CallbackTimeoutException(string message, Exception innerException) : base(message, innerException) { }
}

/// <summary>
/// Thrown only from
/// <see cref="IDurableContext.WaitForCallbackAsync{T}(System.Func{string, IWaitForCallbackContext, System.Threading.Tasks.Task}, string?, WaitForCallbackConfig?, System.Threading.CancellationToken)"/>
/// when the user-supplied submitter delegate (the step that hands the callback
/// ID to the external system) fails after retries are exhausted. Wraps the
/// underlying <see cref="StepException"/> as <see cref="System.Exception.InnerException"/>.
/// </summary>
public class CallbackSubmitterException : CallbackException
{
/// <summary>Creates an empty <see cref="CallbackSubmitterException"/>.</summary>
public CallbackSubmitterException() { }

/// <summary>Creates a <see cref="CallbackSubmitterException"/> with the given message.</summary>
public CallbackSubmitterException(string message) : base(message) { }

/// <summary>Creates a <see cref="CallbackSubmitterException"/> wrapping an inner exception.</summary>
public CallbackSubmitterException(string message, Exception innerException) : base(message, innerException) { }
}
Loading
Loading