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
69 changes: 62 additions & 7 deletions Docs/durable-execution-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1108,18 +1108,40 @@ 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 reflection-based <c>System.Text.Json</c>; an AOT-safe
/// overload taking <c>ICheckpointSerializer&lt;T&gt;</c> is also provided.
/// </summary>
[RequiresUnreferencedCode("Reflection-based JSON for T. Use the ICheckpointSerializer<T> overload for AOT/trimmed deployments.")]
[RequiresDynamicCode("Reflection-based JSON for T. Use the ICheckpointSerializer<T> overload for AOT/trimmed deployments.")]
Task<ICallback<T>> CreateCallbackAsync<T>(
string? name = null,
CallbackConfig? config = null,
CancellationToken cancellationToken = default);

/// <summary>AOT-safe overload of <c>CreateCallbackAsync</c>.</summary>
Task<ICallback<T>> CreateCallbackAsync<T>(
ICheckpointSerializer<T> serializer,
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>
[RequiresUnreferencedCode("Reflection-based JSON for T. Use the ICheckpointSerializer<T> overload for AOT/trimmed deployments.")]
[RequiresDynamicCode("Reflection-based JSON for T. Use the ICheckpointSerializer<T> overload for AOT/trimmed deployments.")]
Task<T> WaitForCallbackAsync<T>(
Func<string, ICallbackContext, Task> submitter,
Func<string, IWaitForCallbackContext, Task> submitter,
string? name = null,
WaitForCallbackConfig? config = null,
CancellationToken cancellationToken = default);

/// <summary>AOT-safe overload of <c>WaitForCallbackAsync</c>.</summary>
Task<T> WaitForCallbackAsync<T>(
Func<string, IWaitForCallbackContext, Task> submitter,
ICheckpointSerializer<T> serializer,
string? name = null,
WaitForCallbackConfig? config = null,
CancellationToken cancellationToken = default);
Expand Down Expand Up @@ -1208,6 +1230,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 @@ -1568,7 +1604,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 +1643,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
namespace Amazon.Lambda.DurableExecution;

/// <summary>
/// Configuration for callback operations created via
/// <see cref="IDurableContext.CreateCallbackAsync{T}(string?, CallbackConfig?, System.Threading.CancellationToken)"/>.
/// </summary>
/// <remarks>
/// Custom serializers are not supplied here — pass an
/// <see cref="ICheckpointSerializer{T}"/> directly to the AOT-safe
/// <c>CreateCallbackAsync</c> overload instead, matching the established
/// <c>StepAsync</c> pattern.
/// </remarks>
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,18 @@
namespace Amazon.Lambda.DurableExecution;

/// <summary>
/// Configuration for the composite
/// <see cref="IDurableContext.WaitForCallbackAsync{T}(System.Func{string, IWaitForCallbackContext, System.Threading.Tasks.Task}, string?, WaitForCallbackConfig?, System.Threading.CancellationToken)"/>
/// operation. Inherits the callback's <see cref="CallbackConfig.Timeout"/> and
/// <see cref="CallbackConfig.HeartbeatTimeout"/>; adds a
/// <see cref="RetryStrategy"/> for the submitter step.
/// </summary>
public class WaitForCallbackConfig : CallbackConfig
{
/// <summary>
/// Retry strategy applied to the submitter step. When null (default),
/// submitter failures are not retried — the submitter step fails terminally
/// and surfaces as <see cref="CallbackSubmitterException"/>.
/// </summary>
public IRetryStrategy? RetryStrategy { get; set; }
}
Loading
Loading