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
4 changes: 4 additions & 0 deletions .autover/autover.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@
"Name": "Amazon.Lambda.Core",
"Path": "Libraries/src/Amazon.Lambda.Core/Amazon.Lambda.Core.csproj"
},
{
"Name": "Amazon.Lambda.DurableExecution",
"Path": "Libraries/src/Amazon.Lambda.DurableExecution/Amazon.Lambda.DurableExecution.csproj"
},
{
"Name": "Amazon.Lambda.DynamoDBEvents",
"Path": "Libraries/src/Amazon.Lambda.DynamoDBEvents/Amazon.Lambda.DynamoDBEvents.csproj"
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ The available projects are:
* Amazon.Lambda.ConfigEvents
* Amazon.Lambda.ConnectEvents
* Amazon.Lambda.Core
* Amazon.Lambda.DurableExecution
* Amazon.Lambda.DynamoDBEvents
* Amazon.Lambda.DynamoDBEvents.SDK.Convertor
* Amazon.Lambda.KafkaEvents
Expand Down
2,228 changes: 2,228 additions & 0 deletions Docs/durable-execution-design.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\..\..\buildtools\common.props" />

<PropertyGroup>
<TargetFrameworks>$(DefaultPackageTargets)</TargetFrameworks>
<Description>Amazon Lambda .NET SDK for Durable Execution - write multi-step workflows that persist state automatically.</Description>
<AssemblyTitle>Amazon.Lambda.DurableExecution</AssemblyTitle>
<Version>0.1.0</Version>
<AssemblyName>Amazon.Lambda.DurableExecution</AssemblyName>
<PackageId>Amazon.Lambda.DurableExecution</PackageId>
<PackageTags>AWS;Amazon;Lambda;Durable;Workflow</PackageTags>
<IsTrimmable>true</IsTrimmable>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors>IL2026,IL2067,IL2075,IL3050</WarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Amazon.Lambda.DurableExecution.Tests, PublicKey="0024000004800000940000000602000000240000525341310004000001000100db5f59f098d27276c7833875a6263a3cc74ab17ba9a9df0b52aedbe7252745db7274d5271fd79c1f08f668ecfa8eaab5626fa76adc811d3c8fc55859b0d09d3bc0a84eecd0ba891f2b8a2fc55141cdcc37c2053d53491e650a479967c3622762977900eddbf1252ed08a2413f00a28f3a0752a81203f03ccb7f684db373518b4"</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Amazon.Lambda.Core\Amazon.Lambda.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.Lambda" Version="4.0.13.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Amazon.Lambda.DurableExecution;

/// <summary>
/// Determines whether a failed step should be retried and with what delay.
/// </summary>
public interface IRetryStrategy
{
/// <summary>
/// Evaluates whether the given exception warrants a retry.
/// </summary>
/// <param name="exception">The exception that caused the step to fail.</param>
/// <param name="attemptNumber">The 1-based attempt number that just failed.</param>
/// <returns>A decision indicating whether to retry and the delay before the next attempt.</returns>
RetryDecision ShouldRetry(Exception exception, int attemptNumber);
}

/// <summary>
/// The outcome of a retry evaluation.
/// </summary>
public readonly struct RetryDecision
{
/// <summary>Whether the step should be retried.</summary>
public bool ShouldRetry { get; }

/// <summary>The delay before the next retry attempt.</summary>
public TimeSpan Delay { get; }

private RetryDecision(bool shouldRetry, TimeSpan delay)
{
ShouldRetry = shouldRetry;
Delay = delay;
}

/// <summary>Indicates the step should not be retried.</summary>
public static RetryDecision DoNotRetry() => new(false, TimeSpan.Zero);

/// <summary>Indicates the step should be retried after the specified delay.</summary>
public static RetryDecision RetryAfter(TimeSpan delay) => new(true, delay);
}
185 changes: 185 additions & 0 deletions Libraries/src/Amazon.Lambda.DurableExecution/Config/RetryStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using System.Text.RegularExpressions;

namespace Amazon.Lambda.DurableExecution;

/// <summary>
/// Jitter strategy for exponential backoff to prevent thundering-herd scenarios.
/// </summary>
public enum JitterStrategy
{
/// <summary>No randomization — delay is exactly the calculated backoff value.</summary>
None,
/// <summary>Random delay between 0 and the calculated backoff value (recommended).</summary>
Full,
/// <summary>Random delay between 50% and 100% of the calculated backoff value.</summary>
Half
}

/// <summary>
/// Controls whether a step re-executes if the Lambda is re-invoked mid-attempt.
/// </summary>
public enum StepSemantics
{
/// <summary>
/// Default. The step may re-execute if the Lambda is re-invoked during execution.
/// Use for idempotent operations.
/// </summary>
AtLeastOncePerRetry,

/// <summary>
/// The step executes at most once per retry attempt. A START checkpoint is written
/// before execution; on replay with an existing START, the SDK skips re-execution
/// and proceeds to the retry handler.
/// </summary>
AtMostOncePerRetry
}

/// <summary>
/// Factory methods for common retry strategies.
/// </summary>
public static class RetryStrategy
{
/// <summary>6 attempts, 2x backoff, 5s initial delay, 60s max, Full jitter.</summary>
public static IRetryStrategy Default { get; } = Exponential(
maxAttempts: 6,
initialDelay: TimeSpan.FromSeconds(5),
maxDelay: TimeSpan.FromSeconds(60),
backoffRate: 2.0,
jitter: JitterStrategy.Full);

/// <summary>3 attempts, 2x backoff, 1s initial delay, 5s max, Half jitter.</summary>
public static IRetryStrategy Transient { get; } = Exponential(
maxAttempts: 3,
initialDelay: TimeSpan.FromSeconds(1),
maxDelay: TimeSpan.FromSeconds(5),
backoffRate: 2.0,
jitter: JitterStrategy.Half);

/// <summary>No retry — 1 attempt only.</summary>
public static IRetryStrategy None { get; } = Exponential(maxAttempts: 1);

/// <summary>
/// Creates an exponential backoff retry strategy.
/// </summary>
public static IRetryStrategy Exponential(
int maxAttempts = 3,
TimeSpan? initialDelay = null,
TimeSpan? maxDelay = null,
double backoffRate = 2.0,
JitterStrategy jitter = JitterStrategy.Full,
Type[]? retryableExceptions = null,
string[]? retryableMessagePatterns = null)
{
return new ExponentialRetryStrategy(
maxAttempts,
initialDelay ?? TimeSpan.FromSeconds(5),
maxDelay ?? TimeSpan.FromSeconds(300),
backoffRate,
jitter,
retryableExceptions,
retryableMessagePatterns);
}

/// <summary>
/// Creates a retry strategy from a delegate.
/// </summary>
public static IRetryStrategy FromDelegate(Func<Exception, int, RetryDecision> strategy)
=> new DelegateRetryStrategy(strategy);
}

internal sealed class ExponentialRetryStrategy : IRetryStrategy
{
private readonly int _maxAttempts;
private readonly TimeSpan _initialDelay;
private readonly TimeSpan _maxDelay;
private readonly double _backoffRate;
private readonly JitterStrategy _jitter;
private readonly Type[]? _retryableExceptions;
private readonly Regex[]? _retryableMessagePatterns;

[ThreadStatic]
private static Random? t_random;
private static Random Random => t_random ??= new Random();

public ExponentialRetryStrategy(
int maxAttempts,
TimeSpan initialDelay,
TimeSpan maxDelay,
double backoffRate,
JitterStrategy jitter,
Type[]? retryableExceptions,
string[]? retryableMessagePatterns)
{
_maxAttempts = maxAttempts;
_initialDelay = initialDelay;
_maxDelay = maxDelay;
_backoffRate = backoffRate;
_jitter = jitter;
_retryableExceptions = retryableExceptions;
_retryableMessagePatterns = retryableMessagePatterns?
.Select(p => new Regex(p, RegexOptions.Compiled))
.ToArray();
}

public RetryDecision ShouldRetry(Exception exception, int attemptNumber)
{
if (attemptNumber >= _maxAttempts)
return RetryDecision.DoNotRetry();

if (!IsRetryable(exception))
return RetryDecision.DoNotRetry();

var delay = CalculateDelay(attemptNumber);
return RetryDecision.RetryAfter(delay);
}

private bool IsRetryable(Exception exception)
{
if (_retryableExceptions == null && _retryableMessagePatterns == null)
return true;

if (_retryableExceptions != null)
{
var exType = exception.GetType();
if (_retryableExceptions.Any(t => t.IsAssignableFrom(exType)))
return true;
}

if (_retryableMessagePatterns != null)
{
var message = exception.Message;
if (_retryableMessagePatterns.Any(p => p.IsMatch(message)))
return true;
}

return false;
}

internal TimeSpan CalculateDelay(int attemptNumber)
{
var baseDelay = _initialDelay.TotalSeconds * Math.Pow(_backoffRate, attemptNumber - 1);
var cappedDelay = Math.Min(baseDelay, _maxDelay.TotalSeconds);

var finalDelay = _jitter switch
{
JitterStrategy.Full => Random.NextDouble() * cappedDelay,
JitterStrategy.Half => cappedDelay * (0.5 + 0.5 * Random.NextDouble()),
_ => cappedDelay
};

return TimeSpan.FromSeconds(Math.Max(1, Math.Ceiling(finalDelay)));
}
}

internal sealed class DelegateRetryStrategy : IRetryStrategy
{
private readonly Func<Exception, int, RetryDecision> _strategy;

public DelegateRetryStrategy(Func<Exception, int, RetryDecision> strategy)
{
_strategy = strategy;
}

public RetryDecision ShouldRetry(Exception exception, int attemptNumber)
=> _strategy(exception, attemptNumber);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Amazon.Lambda.DurableExecution;

/// <summary>
/// Configuration for step execution.
/// </summary>
public sealed class StepConfig
{
/// <summary>
/// Retry strategy for failed steps. When null (default), failures are not retried.
/// </summary>
public IRetryStrategy? RetryStrategy { get; set; }

/// <summary>
/// Controls whether a step may re-execute if the Lambda is re-invoked mid-attempt.
/// Default is <see cref="StepSemantics.AtLeastOncePerRetry"/>.
/// </summary>
public StepSemantics Semantics { get; set; } = StepSemantics.AtLeastOncePerRetry;
}
Loading
Loading