Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat(parameters): enhance thread safety with immutable cache and atom…
…ic operations

- Replace mutable CacheObject properties with immutable read-only fields to prevent concurrent modification issues
- Change CacheManager.Set() to use atomic AddOrUpdate instead of TryGetValue/TryAdd pattern for thread-safe cache updates
- Convert CacheObject to sealed class and make Value and ExpiryTime properties read-only
- Replace nullable TimeSpan _defaultMaxAge with long _defaultMaxAgeTicks for Interlocked compatibility
- Convert _raiseTransformationError from bool to volatile int for Interlocked operations
- Mark _cache and _transformManager fields as volatile to ensure visibility across threads
- Use Interlocked.Exchange and Interlocked.Read for atomic access to _defaultMaxAgeTicks
- Add comprehensive concurrency tests for cache thread safety, parameter retrieval, async context, and transformer operations
- Update InternalsVisibleTo to expose new test helpers for concurrency testing
- Ensures Parameters module is safe for concurrent access in multi-threaded Lambda environments
  • Loading branch information
hjgraca committed Dec 17, 2025
commit 0f88ce9e57e86bf68a92418a684a9e3b450d4619
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ internal CacheManager(IDateTimeWrapper dateTimeWrapper)

/// <summary>
/// Adds a value to the cache by key for a specific duration.
/// Uses atomic AddOrUpdate to ensure thread-safety during concurrent access.
/// </summary>
/// <param name="key">The key to store the value.</param>
/// <param name="value">The value to store.</param>
Expand All @@ -75,14 +76,11 @@ public void Set(string key, object? value, TimeSpan duration)
if (string.IsNullOrWhiteSpace(key) || value is null)
return;

if (_cache.TryGetValue(key, out var cacheObject))
{
cacheObject.Value = value;
cacheObject.ExpiryTime = _dateTimeWrapper.UtcNow.Add(duration);
}
else
{
_cache.TryAdd(key, new CacheObject(value, _dateTimeWrapper.UtcNow.Add(duration)));
}
// Use AddOrUpdate for atomic operation - creates new immutable CacheObject instances
// instead of mutating existing ones to ensure thread-safety
_cache.AddOrUpdate(
key,
_ => new CacheObject(value, _dateTimeWrapper.UtcNow.Add(duration)),
(_, _) => new CacheObject(value, _dateTimeWrapper.UtcNow.Add(duration)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,19 @@ namespace AWS.Lambda.Powertools.Parameters.Internal.Cache;

/// <summary>
/// Class CacheObject.
/// Immutable class to ensure thread-safety when cached values are accessed concurrently.
/// </summary>
internal class CacheObject
internal sealed class CacheObject
{
/// <summary>
/// The value to cache.
/// </summary>
internal object Value { get; set; }
internal object Value { get; }

/// <summary>
/// The expiry time.
/// </summary>
internal DateTime ExpiryTime { get; set; }
internal DateTime ExpiryTime { get; }

/// <summary>
/// CacheObject constructor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,29 @@ internal class ParameterProviderBaseHandler : IParameterProviderBaseHandler

/// <summary>
/// The CacheManager instance.
/// Thread-safe: volatile ensures visibility across threads.
/// </summary>
private ICacheManager? _cache;
private volatile ICacheManager? _cache;

/// <summary>
/// The TransformerManager instance.
/// Thread-safe: volatile ensures visibility across threads.
/// </summary>
private ITransformerManager? _transformManager;
private volatile ITransformerManager? _transformManager;

/// <summary>
/// The DefaultMaxAge.
/// The DefaultMaxAge stored as ticks for thread-safe access.
/// Thread-safe: accessed via Interlocked operations.
/// A value of 0 indicates null/not set.
/// </summary>
private TimeSpan? _defaultMaxAge;
private long _defaultMaxAgeTicks;

/// <summary>
/// The flag to raise exception on transformation error.
/// Thread-safe: volatile ensures visibility across threads.
/// Using int (0/1) instead of bool for Interlocked compatibility.
/// </summary>
private bool _raiseTransformationError;
private volatile int _raiseTransformationError;

/// <summary>
/// The CacheMode.
Expand Down Expand Up @@ -100,6 +106,7 @@ internal ParameterProviderBaseHandler(GetAsyncDelegate getAsyncHandler,

/// <summary>
/// Try transform a value using a transformer.
/// Thread-safe: reads volatile field for raise error flag.
/// </summary>
/// <param name="transformer">The transformer instance to use.</param>
/// <param name="value">The value to transform.</param>
Expand All @@ -121,7 +128,7 @@ internal ParameterProviderBaseHandler(GetAsyncDelegate getAsyncHandler,
catch (Exception e)
{
transformedValue = default;
if (_raiseTransformationError)
if (_raiseTransformationError != 0)
{
if (e is not TransformationException error)
error = new TransformationException(e.Message, e);
Expand All @@ -140,32 +147,37 @@ internal ParameterProviderBaseHandler(GetAsyncDelegate getAsyncHandler,

/// <summary>
/// Sets the cache maximum age.
/// Thread-safe: uses Interlocked for atomic write.
/// </summary>
/// <param name="maxAge">The cache maximum age </param>
public void SetDefaultMaxAge(TimeSpan maxAge)
{
_defaultMaxAge = maxAge;
Interlocked.Exchange(ref _defaultMaxAgeTicks, maxAge.Ticks);
}

/// <summary>
/// Gets the maximum age or default value.
/// Thread-safe: uses Interlocked for atomic read.
/// </summary>
/// <returns>the maxAge</returns>
public TimeSpan? GetDefaultMaxAge()
{
return _defaultMaxAge;
var ticks = Interlocked.Read(ref _defaultMaxAgeTicks);
return ticks > 0 ? TimeSpan.FromTicks(ticks) : null;
}

/// <summary>
/// Gets the maximum age or default value.
/// Thread-safe: uses Interlocked for atomic read.
/// </summary>
/// <param name="config"></param>
/// <returns>the maxAge</returns>
public TimeSpan GetMaxAge(ParameterProviderConfiguration? config)
{
var maxAge = config?.MaxAge;
if (maxAge.HasValue && maxAge.Value > TimeSpan.Zero) return maxAge.Value;
if (_defaultMaxAge.HasValue && _defaultMaxAge.Value > TimeSpan.Zero) return _defaultMaxAge.Value;
var defaultMaxAgeTicks = Interlocked.Read(ref _defaultMaxAgeTicks);
if (defaultMaxAgeTicks > 0) return TimeSpan.FromTicks(defaultMaxAgeTicks);
return CacheManager.DefaultMaxAge;
}

Expand Down Expand Up @@ -208,11 +220,12 @@ public void AddCustomTransformer(string name, ITransformer transformer)

/// <summary>
/// Configure the transformer to raise exception or return Null on transformation error
/// Thread-safe: uses volatile field for visibility.
/// </summary>
/// <param name="raiseError">true for raise error, false for return Null.</param>
public void SetRaiseTransformationError(bool raiseError)
{
_raiseTransformationError = raiseError;
_raiseTransformationError = raiseError ? 1 : 0;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ namespace AWS.Lambda.Powertools.Parameters.Internal.Transform;
internal class TransformerManager : ITransformerManager
{
/// <summary>
/// The TransformerManager instance.
/// Thread-safe lazy initialization of the TransformerManager singleton instance.
/// Uses LazyThreadSafetyMode.ExecutionAndPublication to ensure only one instance
/// is created even under concurrent access from multiple threads.
/// </summary>
private static ITransformerManager? _instance;
private static readonly Lazy<ITransformerManager> _lazyInstance =
new Lazy<ITransformerManager>(() => new TransformerManager(), LazyThreadSafetyMode.ExecutionAndPublication);

/// <summary>
/// The JsonTransformer instance.
Expand All @@ -39,9 +42,9 @@ internal class TransformerManager : ITransformerManager
private readonly ITransformer _base64Transformer;

/// <summary>
/// Gets the TransformerManager instance.
/// Gets the TransformerManager instance in a thread-safe manner.
/// </summary>
internal static ITransformerManager Instance => _instance ??= new TransformerManager();
internal static ITransformerManager Instance => _lazyInstance.Value;

/// <summary>
/// Gets the list of transformer instances.
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.Parameters.Tests")]
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Parameters.Tests")]
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.ConcurrencyTests")]
Loading
Loading