Skip to content
Merged
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
22 changes: 18 additions & 4 deletions libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Text.Json.Serialization;
using System.Threading;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
Expand All @@ -16,6 +17,12 @@ namespace AWS.Lambda.Powertools.Idempotency;
/// </summary>
public sealed class Idempotency
{
/// <summary>
/// AsyncLocal storage for per-invocation LambdaContext.
/// This ensures each concurrent Lambda invocation has its own isolated context.
/// </summary>
private static readonly AsyncLocal<ILambdaContext> _lambdaContext = new();

/// <summary>
/// The general configurations for the idempotency
/// </summary>
Expand Down Expand Up @@ -66,18 +73,25 @@ public static void Configure(Action<IdempotencyBuilder> configurationAction)
}

/// <summary>
/// Holds ILambdaContext
/// Holds ILambdaContext using AsyncLocal for per-invocation isolation.
/// Each concurrent Lambda invocation will have its own isolated context.
/// </summary>
public ILambdaContext LambdaContext { get; private set; }
public ILambdaContext LambdaContext
{
get => _lambdaContext.Value;
private set => _lambdaContext.Value = value;
}

/// <summary>
/// Can be used in a method which is not the handler to capture the Lambda context,
/// to calculate the remaining time before the invocation times out.
/// This method is thread-safe and stores the context in AsyncLocal storage,
/// ensuring isolation between concurrent Lambda invocations.
/// </summary>
/// <param name="context"></param>
/// <param name="context">The Lambda context for the current invocation</param>
public static void RegisterLambdaContext(ILambdaContext context)
{
Instance.LambdaContext = context;
_lambdaContext.Value = context;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Idempotency.Tests")]
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Idempotency.Tests")]
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.ConcurrencyTests")]
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence;
/// </summary>
public abstract class BasePersistenceStore : IPersistenceStore
{
/// <summary>
/// Lock object for thread-safe configuration
/// </summary>
private readonly object _configureLock = new object();

/// <summary>
/// Flag indicating whether the store has been configured
/// </summary>
private volatile bool _isConfigured;

/// <summary>
/// Idempotency Options
/// </summary>
Expand All @@ -39,50 +49,95 @@ public abstract class BasePersistenceStore : IPersistenceStore
private LRUCache<string, DataRecord> _cache = null!;

/// <summary>
/// Initialize the base persistence layer from the configuration settings
/// Initialize the base persistence layer from the configuration settings.
/// This method is thread-safe and idempotent - multiple calls with the same parameters are safe.
/// </summary>
/// <param name="idempotencyOptions">Idempotency configuration settings</param>
/// <param name="functionName">The name of the function being decorated</param>
/// <param name="keyPrefix"></param>
public void Configure(IdempotencyOptions idempotencyOptions, string functionName, string keyPrefix)
{
if (!string.IsNullOrEmpty(keyPrefix))
// Fast path - already configured
if (_isConfigured) return;

lock (_configureLock)
{
_functionName = keyPrefix;
}
else
{
var funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv);

_functionName = funcEnv ?? "testFunction";
if (!string.IsNullOrWhiteSpace(functionName))
// Double-check pattern
if (_isConfigured) return;

if (!string.IsNullOrEmpty(keyPrefix))
{
_functionName += "." + functionName;
_functionName = keyPrefix;
}
}
else
{
var funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv);

_idempotencyOptions = idempotencyOptions;
_functionName = funcEnv ?? "testFunction";
if (!string.IsNullOrWhiteSpace(functionName))
{
_functionName += "." + functionName;
}
}

if (!string.IsNullOrWhiteSpace(_idempotencyOptions.PayloadValidationJmesPath))
{
PayloadValidationEnabled = true;
}
_idempotencyOptions = idempotencyOptions;

var useLocalCache = _idempotencyOptions.UseLocalCache;
if (useLocalCache)
{
_cache = new LRUCache<string, DataRecord>(_idempotencyOptions.LocalCacheMaxItems);
if (!string.IsNullOrWhiteSpace(_idempotencyOptions.PayloadValidationJmesPath))
{
PayloadValidationEnabled = true;
}

var useLocalCache = _idempotencyOptions.UseLocalCache;
if (useLocalCache)
{
_cache = new LRUCache<string, DataRecord>(_idempotencyOptions.LocalCacheMaxItems);
}

_isConfigured = true;
}
}

/// <summary>
/// For test purpose only (adding a cache to mock)
/// For test purpose only (adding a cache to mock).
/// This method is thread-safe and idempotent.
/// </summary>
internal void Configure(IdempotencyOptions options, string functionName, string keyPrefix,
LRUCache<string, DataRecord> cache)
{
Configure(options, functionName, keyPrefix);
_cache = cache;
// Fast path - already configured
if (_isConfigured) return;

lock (_configureLock)
{
// Double-check pattern
if (_isConfigured) return;

if (!string.IsNullOrEmpty(keyPrefix))
{
_functionName = keyPrefix;
}
else
{
var funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv);

_functionName = funcEnv ?? "testFunction";
if (!string.IsNullOrWhiteSpace(functionName))
{
_functionName += "." + functionName;
}
}

_idempotencyOptions = options;

if (!string.IsNullOrWhiteSpace(_idempotencyOptions.PayloadValidationJmesPath))
{
PayloadValidationEnabled = true;
}

_cache = cache;

_isConfigured = true;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Amazon.Lambda.TestUtilities" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\AWS.Lambda.Powertools.Idempotency\AWS.Lambda.Powertools.Idempotency.csproj" />
<ProjectReference Include="..\..\src\AWS.Lambda.Powertools.Logging\AWS.Lambda.Powertools.Logging.csproj" />
<ProjectReference Include="..\..\src\AWS.Lambda.Powertools.Metrics\AWS.Lambda.Powertools.Metrics.csproj" />
<ProjectReference Include="..\..\src\AWS.Lambda.Powertools.Tracing\AWS.Lambda.Powertools.Tracing.csproj" />
Expand Down
Loading
Loading