Skip to content

Commit 5e8acac

Browse files
authored
chore(parameters): Thread safety (#1098)
1 parent 01366df commit 5e8acac

16 files changed

Lines changed: 4784 additions & 104 deletions

libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheManager.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ internal CacheManager(IDateTimeWrapper dateTimeWrapper)
6666

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

78-
if (_cache.TryGetValue(key, out var cacheObject))
79-
{
80-
cacheObject.Value = value;
81-
cacheObject.ExpiryTime = _dateTimeWrapper.UtcNow.Add(duration);
82-
}
83-
else
84-
{
85-
_cache.TryAdd(key, new CacheObject(value, _dateTimeWrapper.UtcNow.Add(duration)));
86-
}
79+
// Use AddOrUpdate for atomic operation - creates new immutable CacheObject instances
80+
// instead of mutating existing ones to ensure thread-safety
81+
_cache.AddOrUpdate(
82+
key,
83+
_ => new CacheObject(value, _dateTimeWrapper.UtcNow.Add(duration)),
84+
(_, _) => new CacheObject(value, _dateTimeWrapper.UtcNow.Add(duration)));
8785
}
8886
}

libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Cache/CacheObject.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,19 @@ namespace AWS.Lambda.Powertools.Parameters.Internal.Cache;
1717

1818
/// <summary>
1919
/// Class CacheObject.
20+
/// Immutable class to ensure thread-safety when cached values are accessed concurrently.
2021
/// </summary>
21-
internal class CacheObject
22+
internal sealed class CacheObject
2223
{
2324
/// <summary>
2425
/// The value to cache.
2526
/// </summary>
26-
internal object Value { get; set; }
27+
internal object Value { get; }
2728

2829
/// <summary>
2930
/// The expiry time.
3031
/// </summary>
31-
internal DateTime ExpiryTime { get; set; }
32+
internal DateTime ExpiryTime { get; }
3233

3334
/// <summary>
3435
/// CacheObject constructor.

libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Provider/ParameterProviderBaseHandler.cs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,29 @@ internal class ParameterProviderBaseHandler : IParameterProviderBaseHandler
4040

4141
/// <summary>
4242
/// The CacheManager instance.
43+
/// Thread-safe: volatile ensures visibility across threads.
4344
/// </summary>
44-
private ICacheManager? _cache;
45+
private volatile ICacheManager? _cache;
4546

4647
/// <summary>
4748
/// The TransformerManager instance.
49+
/// Thread-safe: volatile ensures visibility across threads.
4850
/// </summary>
49-
private ITransformerManager? _transformManager;
51+
private volatile ITransformerManager? _transformManager;
5052

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

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

6167
/// <summary>
6268
/// The CacheMode.
@@ -100,6 +106,7 @@ internal ParameterProviderBaseHandler(GetAsyncDelegate getAsyncHandler,
100106

101107
/// <summary>
102108
/// Try transform a value using a transformer.
109+
/// Thread-safe: reads volatile field for raise error flag.
103110
/// </summary>
104111
/// <param name="transformer">The transformer instance to use.</param>
105112
/// <param name="value">The value to transform.</param>
@@ -121,7 +128,7 @@ internal ParameterProviderBaseHandler(GetAsyncDelegate getAsyncHandler,
121128
catch (Exception e)
122129
{
123130
transformedValue = default;
124-
if (_raiseTransformationError)
131+
if (_raiseTransformationError != 0)
125132
{
126133
if (e is not TransformationException error)
127134
error = new TransformationException(e.Message, e);
@@ -140,32 +147,37 @@ internal ParameterProviderBaseHandler(GetAsyncDelegate getAsyncHandler,
140147

141148
/// <summary>
142149
/// Sets the cache maximum age.
150+
/// Thread-safe: uses Interlocked for atomic write.
143151
/// </summary>
144152
/// <param name="maxAge">The cache maximum age </param>
145153
public void SetDefaultMaxAge(TimeSpan maxAge)
146154
{
147-
_defaultMaxAge = maxAge;
155+
Interlocked.Exchange(ref _defaultMaxAgeTicks, maxAge.Ticks);
148156
}
149157

150158
/// <summary>
151159
/// Gets the maximum age or default value.
160+
/// Thread-safe: uses Interlocked for atomic read.
152161
/// </summary>
153162
/// <returns>the maxAge</returns>
154163
public TimeSpan? GetDefaultMaxAge()
155164
{
156-
return _defaultMaxAge;
165+
var ticks = Interlocked.Read(ref _defaultMaxAgeTicks);
166+
return ticks > 0 ? TimeSpan.FromTicks(ticks) : null;
157167
}
158168

159169
/// <summary>
160170
/// Gets the maximum age or default value.
171+
/// Thread-safe: uses Interlocked for atomic read.
161172
/// </summary>
162173
/// <param name="config"></param>
163174
/// <returns>the maxAge</returns>
164175
public TimeSpan GetMaxAge(ParameterProviderConfiguration? config)
165176
{
166177
var maxAge = config?.MaxAge;
167178
if (maxAge.HasValue && maxAge.Value > TimeSpan.Zero) return maxAge.Value;
168-
if (_defaultMaxAge.HasValue && _defaultMaxAge.Value > TimeSpan.Zero) return _defaultMaxAge.Value;
179+
var defaultMaxAgeTicks = Interlocked.Read(ref _defaultMaxAgeTicks);
180+
if (defaultMaxAgeTicks > 0) return TimeSpan.FromTicks(defaultMaxAgeTicks);
169181
return CacheManager.DefaultMaxAge;
170182
}
171183

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

209221
/// <summary>
210222
/// Configure the transformer to raise exception or return Null on transformation error
223+
/// Thread-safe: uses volatile field for visibility.
211224
/// </summary>
212225
/// <param name="raiseError">true for raise error, false for return Null.</param>
213226
public void SetRaiseTransformationError(bool raiseError)
214227
{
215-
_raiseTransformationError = raiseError;
228+
_raiseTransformationError = raiseError ? 1 : 0;
216229
}
217230

218231
/// <summary>

libraries/src/AWS.Lambda.Powertools.Parameters/Internal/Transform/TransformerManager.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ namespace AWS.Lambda.Powertools.Parameters.Internal.Transform;
2424
internal class TransformerManager : ITransformerManager
2525
{
2626
/// <summary>
27-
/// The TransformerManager instance.
27+
/// Thread-safe lazy initialization of the TransformerManager singleton instance.
28+
/// Uses LazyThreadSafetyMode.ExecutionAndPublication to ensure only one instance
29+
/// is created even under concurrent access from multiple threads.
2830
/// </summary>
29-
private static ITransformerManager? _instance;
31+
private static readonly Lazy<ITransformerManager> _lazyInstance =
32+
new Lazy<ITransformerManager>(() => new TransformerManager(), LazyThreadSafetyMode.ExecutionAndPublication);
3033

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

4144
/// <summary>
42-
/// Gets the TransformerManager instance.
45+
/// Gets the TransformerManager instance in a thread-safe manner.
4346
/// </summary>
44-
internal static ITransformerManager Instance => _instance ??= new TransformerManager();
47+
internal static ITransformerManager Instance => _lazyInstance.Value;
4548

4649
/// <summary>
4750
/// Gets the list of transformer instances.

libraries/src/AWS.Lambda.Powertools.Parameters/InternalsVisibleTo.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515

1616
using System.Runtime.CompilerServices;
1717

18-
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Parameters.Tests")]
18+
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Parameters.Tests")]
19+
[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.ConcurrencyTests")]

0 commit comments

Comments
 (0)