Skip to content

Commit 2847c8f

Browse files
authored
Merge branch 'develop' into release(1.20)-update-versions
2 parents 362c272 + f2a53ee commit 2847c8f

File tree

13 files changed

+221
-74
lines changed

13 files changed

+221
-74
lines changed

docs/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# v9.1.18
2-
FROM squidfunk/mkdocs-material@sha256:7e841df1cfb6c8c4ff0968f2cfe55127fb1a2f5614e1c9bc23cbc11fe4c96644
2+
FROM squidfunk/mkdocs-material@sha256:c62453b1ba229982c6325a71165c1a3007c11bd3dd470e7a1446c5783bd145b4
33
RUN pip install mkdocs-git-revision-date-plugin

libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ namespace AWS.Lambda.Powertools.Idempotency;
6363
[Injection(typeof(UniversalWrapperAspect), Inherited = true)]
6464
public class IdempotentAttribute : UniversalWrapperAttribute
6565
{
66+
/// <summary>
67+
/// Custom prefix for idempotency key: key_prefix#hash
68+
/// </summary>
69+
public string KeyPrefix { get; set; }
70+
6671
/// <summary>
6772
/// Wraps as a synchronous operation, simply throws IdempotencyConfigurationException
6873
/// </summary>
@@ -90,7 +95,7 @@ protected internal sealed override T WrapSync<T>(Func<object[], T> target, objec
9095

9196
Task<T> ResultDelegate() => Task.FromResult(target(args));
9297

93-
var idempotencyHandler = new IdempotencyAspectHandler<T>(ResultDelegate, eventArgs.Method.Name, payload,GetContext(eventArgs));
98+
var idempotencyHandler = new IdempotencyAspectHandler<T>(ResultDelegate, eventArgs.Method.Name, KeyPrefix, payload,GetContext(eventArgs));
9499
if (idempotencyHandler == null)
95100
{
96101
throw new Exception("Failed to create an instance of IdempotencyAspectHandler");
@@ -128,7 +133,7 @@ protected internal sealed override async Task<T> WrapAsync<T>(
128133

129134
Task<T> ResultDelegate() => target(args);
130135

131-
var idempotencyHandler = new IdempotencyAspectHandler<T>(ResultDelegate, eventArgs.Method.Name, payload, GetContext(eventArgs));
136+
var idempotencyHandler = new IdempotencyAspectHandler<T>(ResultDelegate, eventArgs.Method.Name, KeyPrefix, payload, GetContext(eventArgs));
132137
if (idempotencyHandler == null)
133138
{
134139
throw new Exception("Failed to create an instance of IdempotencyAspectHandler");

libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,21 @@ internal class IdempotencyAspectHandler<T>
5050
/// </summary>
5151
/// <param name="target"></param>
5252
/// <param name="functionName"></param>
53+
/// <param name="keyPrefix"></param>
5354
/// <param name="payload"></param>
5455
/// <param name="lambdaContext"></param>
5556
public IdempotencyAspectHandler(
5657
Func<Task<T>> target,
5758
string functionName,
59+
string keyPrefix,
5860
JsonDocument payload,
5961
ILambdaContext lambdaContext)
6062
{
6163
_target = target;
6264
_data = payload;
6365
_lambdaContext = lambdaContext;
6466
_persistenceStore = Idempotency.Instance.PersistenceStore;
65-
_persistenceStore.Configure(Idempotency.Instance.IdempotencyOptions, functionName);
67+
_persistenceStore.Configure(Idempotency.Instance.IdempotencyOptions, functionName, keyPrefix);
6668
}
6769

6870
/// <summary>

libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/*
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3-
*
3+
*
44
* Licensed under the Apache License, Version 2.0 (the "License").
55
* You may not use this file except in compliance with the License.
66
* A copy of the License is located at
7-
*
7+
*
88
* http://aws.amazon.com/apache2.0
9-
*
9+
*
1010
* or in the "license" file accompanying this file. This file is distributed
1111
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
1212
* express or implied. See the License for the specific language governing
@@ -37,16 +37,17 @@ public abstract class BasePersistenceStore : IPersistenceStore
3737
/// Idempotency Options
3838
/// </summary>
3939
private IdempotencyOptions _idempotencyOptions = null!;
40-
40+
4141
/// <summary>
4242
/// Function name
4343
/// </summary>
4444
private string _functionName;
45+
4546
/// <summary>
4647
/// Boolean to indicate whether or not payload validation is enabled
4748
/// </summary>
4849
protected bool PayloadValidationEnabled;
49-
50+
5051
/// <summary>
5152
/// LRUCache
5253
/// </summary>
@@ -57,34 +58,45 @@ public abstract class BasePersistenceStore : IPersistenceStore
5758
/// </summary>
5859
/// <param name="idempotencyOptions">Idempotency configuration settings</param>
5960
/// <param name="functionName">The name of the function being decorated</param>
60-
public void Configure(IdempotencyOptions idempotencyOptions, string functionName)
61+
/// <param name="keyPrefix"></param>
62+
public void Configure(IdempotencyOptions idempotencyOptions, string functionName, string keyPrefix)
6163
{
62-
var funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv);
63-
_functionName = funcEnv ?? "testFunction";
64-
if (!string.IsNullOrWhiteSpace(functionName))
64+
if (!string.IsNullOrEmpty(keyPrefix))
6565
{
66-
_functionName += "." + functionName;
66+
_functionName = keyPrefix;
6767
}
68+
else
69+
{
70+
var funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv);
71+
72+
_functionName = funcEnv ?? "testFunction";
73+
if (!string.IsNullOrWhiteSpace(functionName))
74+
{
75+
_functionName += "." + functionName;
76+
}
77+
}
78+
6879
_idempotencyOptions = idempotencyOptions;
69-
80+
7081
if (!string.IsNullOrWhiteSpace(_idempotencyOptions.PayloadValidationJmesPath))
7182
{
7283
PayloadValidationEnabled = true;
7384
}
74-
85+
7586
var useLocalCache = _idempotencyOptions.UseLocalCache;
7687
if (useLocalCache)
7788
{
7889
_cache = new LRUCache<string, DataRecord>(_idempotencyOptions.LocalCacheMaxItems);
7990
}
8091
}
81-
92+
8293
/// <summary>
8394
/// For test purpose only (adding a cache to mock)
8495
/// </summary>
85-
internal void Configure(IdempotencyOptions options, string functionName, LRUCache<string, DataRecord> cache)
96+
internal void Configure(IdempotencyOptions options, string functionName, string keyPrefix,
97+
LRUCache<string, DataRecord> cache)
8698
{
87-
Configure(options, functionName);
99+
Configure(options, functionName, keyPrefix);
88100
_cache = cache;
89101
}
90102

@@ -118,12 +130,12 @@ public virtual async Task SaveSuccess(JsonDocument data, object result, DateTime
118130
public virtual async Task SaveInProgress(JsonDocument data, DateTimeOffset now, double? remainingTimeInMs)
119131
{
120132
var idempotencyKey = GetHashedIdempotencyKey(data);
121-
133+
122134
if (RetrieveFromCache(idempotencyKey, now) != null)
123135
{
124136
throw new IdempotencyItemAlreadyExistsException();
125137
}
126-
138+
127139
long? inProgressExpirationMsTimestamp = null;
128140
if (remainingTimeInMs.HasValue)
129141
{
@@ -137,11 +149,10 @@ public virtual async Task SaveInProgress(JsonDocument data, DateTimeOffset now,
137149
null,
138150
GetHashedPayload(data),
139151
inProgressExpirationMsTimestamp
140-
141152
);
142153
await PutRecord(record, now);
143154
}
144-
155+
145156
/// <summary>
146157
/// Delete record from the persistence store
147158
/// </summary>
@@ -152,14 +163,14 @@ public virtual async Task DeleteRecord(JsonDocument data, Exception throwable)
152163
var idemPotencyKey = GetHashedIdempotencyKey(data);
153164

154165
Console.WriteLine("Function raised an exception {0}. " +
155-
"Clearing in progress record in persistence store for idempotency key: {1}",
166+
"Clearing in progress record in persistence store for idempotency key: {1}",
156167
throwable.GetType().Name,
157168
idemPotencyKey);
158169

159170
await DeleteRecord(idemPotencyKey);
160171
DeleteFromCache(idemPotencyKey);
161172
}
162-
173+
163174
/// <summary>
164175
/// Retrieve idempotency key for data provided, fetch from persistence store, and convert to DataRecord.
165176
/// </summary>
@@ -182,7 +193,7 @@ public virtual async Task<DataRecord> GetRecord(JsonDocument data, DateTimeOffse
182193
ValidatePayload(data, record);
183194
return record;
184195
}
185-
196+
186197
/// <summary>
187198
/// Save data_record to local cache except when status is "INPROGRESS"
188199
/// NOTE: We can't cache "INPROGRESS" records as we have no way to reflect updates that can happen outside of the
@@ -198,7 +209,7 @@ private void SaveToCache(DataRecord dataRecord)
198209

199210
_cache.Set(dataRecord.IdempotencyKey, dataRecord);
200211
}
201-
212+
202213
/// <summary>
203214
/// Validate that the hashed payload matches data provided and stored data record
204215
/// </summary>
@@ -215,7 +226,7 @@ private void ValidatePayload(JsonDocument data, DataRecord dataRecord)
215226
throw new IdempotencyValidationException("Payload does not match stored record for this event key");
216227
}
217228
}
218-
229+
219230
/// <summary>
220231
/// Retrieve data record from cache
221232
/// </summary>
@@ -228,14 +239,15 @@ private DataRecord RetrieveFromCache(string idempotencyKey, DateTimeOffset now)
228239
return null;
229240

230241
if (!_cache.TryGet(idempotencyKey, out var record) || record == null) return null;
231-
if (!record.IsExpired(now))
242+
if (!record.IsExpired(now))
232243
{
233244
return record;
234245
}
246+
235247
DeleteFromCache(idempotencyKey);
236248
return null;
237249
}
238-
250+
239251
/// <summary>
240252
/// Deletes item from cache
241253
/// </summary>
@@ -244,10 +256,10 @@ private void DeleteFromCache(string idempotencyKey)
244256
{
245257
if (!_idempotencyOptions.UseLocalCache)
246258
return;
247-
259+
248260
_cache.Delete(idempotencyKey);
249261
}
250-
262+
251263
/// <summary>
252264
/// Extract payload using validation key jmespath and return a hashed representation
253265
/// </summary>
@@ -259,12 +271,12 @@ private string GetHashedPayload(JsonDocument data)
259271
{
260272
return "";
261273
}
262-
274+
263275
var transformer = JsonTransformer.Parse(_idempotencyOptions.PayloadValidationJmesPath);
264276
var result = transformer.Transform(data.RootElement);
265277
return GenerateHash(result.RootElement);
266278
}
267-
279+
268280
/// <summary>
269281
/// Calculate unix timestamp of expiry date for idempotency record
270282
/// </summary>
@@ -285,7 +297,7 @@ private string GetHashedIdempotencyKey(JsonDocument data)
285297
{
286298
var node = data.RootElement;
287299
var eventKeyJmesPath = _idempotencyOptions.EventKeyJmesPath;
288-
if (eventKeyJmesPath != null)
300+
if (eventKeyJmesPath != null)
289301
{
290302
var transformer = JsonTransformer.Parse(eventKeyJmesPath);
291303
var result = transformer.Transform(node);
@@ -298,7 +310,9 @@ private string GetHashedIdempotencyKey(JsonDocument data)
298310
{
299311
throw new IdempotencyKeyException("No data found to create a hashed idempotency key");
300312
}
301-
Console.WriteLine("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyOptions.EventKeyJmesPath ?? string.Empty);
313+
314+
Console.WriteLine("No data found to create a hashed idempotency key. JMESPath: {0}",
315+
_idempotencyOptions.EventKeyJmesPath ?? string.Empty);
302316
}
303317

304318
var hash = GenerateHash(node);
@@ -313,9 +327,10 @@ private string GetHashedIdempotencyKey(JsonDocument data)
313327
private static bool IsMissingIdempotencyKey(JsonElement data)
314328
{
315329
return data.ValueKind == JsonValueKind.Null || data.ValueKind == JsonValueKind.Undefined
316-
|| (data.ValueKind == JsonValueKind.String && data.ToString() == string.Empty);
330+
|| (data.ValueKind == JsonValueKind.String &&
331+
data.ToString() == string.Empty);
317332
}
318-
333+
319334
/// <summary>
320335
/// Generate a hash value from the provided data
321336
/// </summary>
@@ -328,16 +343,16 @@ internal string GenerateHash(JsonElement data)
328343
// starting .NET 8 no option to change hash algorithm
329344
using var hashAlgorithm = MD5.Create();
330345
#else
331-
332346
using var hashAlgorithm = HashAlgorithm.Create(_idempotencyOptions.HashFunction);
333347
#endif
334348
if (hashAlgorithm == null)
335349
{
336350
throw new ArgumentException("Invalid HashAlgorithm");
337351
}
352+
338353
var stringToHash = data.ToString();
339354
var hash = GetHash(hashAlgorithm, stringToHash);
340-
355+
341356
return hash;
342357
}
343358

@@ -351,18 +366,18 @@ private static string GetHash(HashAlgorithm hashAlgorithm, string input)
351366
{
352367
// Convert the input string to a byte array and compute the hash.
353368
var data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input));
354-
369+
355370
// Create a new Stringbuilder to collect the bytes
356371
// and create a string.
357372
var sBuilder = new StringBuilder();
358-
373+
359374
// Loop through each byte of the hashed data
360375
// and format each one as a hexadecimal string.
361376
for (var i = 0; i < data.Length; i++)
362377
{
363378
sBuilder.Append(data[i].ToString("x2"));
364379
}
365-
380+
366381
// Return the hexadecimal string.
367382
return sBuilder.ToString();
368383
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System;
2+
using Amazon.Lambda.Core;
3+
4+
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
5+
6+
/// <summary>
7+
/// Simple Lambda function with Idempotent attribute on a sub method with a custom prefix key
8+
/// </summary>
9+
public class IdempotencyAttributeWithCustomKeyPrefix
10+
{
11+
public string HandleRequest(string input, ILambdaContext context)
12+
{
13+
return ReturnGuid(input);
14+
}
15+
16+
[Idempotent(KeyPrefix = "MyMethod")]
17+
private string ReturnGuid(string p)
18+
{
19+
return Guid.NewGuid().ToString();
20+
}
21+
}

libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunctionMethodDecorated.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
* permissions and limitations under the License.
1414
*/
1515

16-
using System;
1716
using System.Collections.Generic;
1817
using System.IO;
1918
using System.Net.Http;
@@ -22,7 +21,6 @@
2221
using Amazon.DynamoDBv2;
2322
using Amazon.Lambda.APIGatewayEvents;
2423
using Amazon.Lambda.Core;
25-
using AWS.Lambda.Powertools.Idempotency.Tests.Model;
2624

2725
namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers;
2826

@@ -34,9 +32,6 @@ public IdempotencyFunctionMethodDecorated(AmazonDynamoDBClient client)
3432
{
3533
Idempotency.Configure(builder =>
3634
builder
37-
#if NET8_0_OR_GREATER
38-
.WithJsonSerializationContext(TestJsonSerializerContext.Default)
39-
#endif
4035
.UseDynamoDb(storeBuilder =>
4136
storeBuilder
4237
.WithTableName("idempotency_table")

0 commit comments

Comments
 (0)