Skip to content

chore: AOT support for Idempotency #653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8a889ca
Add JMESPathSerializationContext.cs and Serializer. Add Idempotency I…
hjgraca Sep 26, 2024
2c3b7ba
fix failing tests
hjgraca Sep 27, 2024
a4606e8
add tests
hjgraca Sep 27, 2024
d6f7b43
tackle sonar rules
hjgraca Sep 27, 2024
8df9dce
Merge branch 'develop' of https://github.com/hjgraca/powertools-lambd…
hjgraca Sep 27, 2024
7f5bd66
update tests. Instance is now internal
hjgraca Sep 30, 2024
5064e44
Merge branch 'develop' of https://github.com/hjgraca/powertools-lambd…
hjgraca Oct 2, 2024
1c2c997
check IsDynamicCodeSupported to avoid TypeInfo in non AOT
hjgraca Oct 3, 2024
6e2537c
Merge branch 'develop' of https://github.com/hjgraca/powertools-lambd…
hjgraca Oct 6, 2024
e7f53aa
ignore warnings
hjgraca Oct 8, 2024
58bb687
Merge branch 'develop' into aot(idempotency|jmespath)-aot-support
hjgraca Jan 27, 2025
b6ccc7c
Merge branch 'develop' of https://github.com/hjgraca/powertools-lambd…
hjgraca Feb 3, 2025
6ee247c
Merge branch 'develop' into aot(idempotency|jmespath)-aot-support
hjgraca Feb 3, 2025
f36c7ab
Add AOT support for Idempotency utility and related tests
hjgraca Feb 4, 2025
cc6d784
Merge branch 'develop' of https://github.com/hjgraca/powertools-lambd…
hjgraca Feb 4, 2025
3b1cd37
Merge remote-tracking branch 'origin/aot(idempotency|jmespath)-aot-su…
hjgraca Feb 4, 2025
41fc2f5
feat(tests): add unit tests for IdempotencySerializer and update JSON…
hjgraca Feb 4, 2025
af59143
fix function name
hjgraca Feb 4, 2025
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
67 changes: 67 additions & 0 deletions docs/utilities/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The idempotency utility provides a simple solution to convert your Lambda functi
* Select a subset of the event as the idempotency key using [JMESPath](https://jmespath.org/) expressions
* Set a time window in which records with the same payload should be considered duplicates
* Expires in-progress executions if the Lambda function times out halfway through
* Ahead-of-Time compilation to native code support [AOT](https://docs.aws.amazon.com/lambda/latest/dg/dotnet-native-aot.html) from version 1.3.0

## Terminology

Expand Down Expand Up @@ -821,10 +822,76 @@ Data would then be stored in DynamoDB like this:
| idempotency#MyLambdaFunction | 2b2cdb5f86361e97b4383087c1ffdf27 | 1636549571 | COMPLETED | {"id": 527212, "message": "success"} |
| idempotency#MyLambdaFunction | f091d2527ad1c78f05d54cc3f363be80 | 1636549585 | IN_PROGRESS | |


## AOT Support

Native AOT trims your application code as part of the compilation to ensure that the binary is as small as possible. .NET 8 for Lambda provides improved trimming support compared to previous versions of .NET.

### WithJsonSerializationContext()

To use Idempotency utility with AOT support you first need to add `WithJsonSerializationContext()` to your `Idempotency` configuration.

This ensures that when serializing your payload, the utility uses the correct serialization context.

In the example below, we use the default `LambdaFunctionJsonSerializerContext`:

```csharp
Idempotency.Configure(builder =>
builder.WithJsonSerializationContext(LambdaFunctionJsonSerializerContext.Default)));

```

Full example:

```csharp hl_lines="8"
public static class Function
{
private static async Task Main()
{
var tableName = Environment.GetEnvironmentVariable("IDEMPOTENCY_TABLE_NAME");
Idempotency.Configure(builder =>
builder
.WithJsonSerializationContext(LambdaFunctionJsonSerializerContext.Default)
.WithOptions(optionsBuilder => optionsBuilder
.WithExpiration(TimeSpan.FromHours(1)))
.UseDynamoDb(storeBuilder => storeBuilder
.WithTableName(tableName)
));

Func<APIGatewayProxyRequest, ILambdaContext, APIGatewayProxyResponse> handler = FunctionHandler;
await LambdaBootstrapBuilder.Create(handler,
new SourceGeneratorLambdaJsonSerializer<LambdaFunctionJsonSerializerContext>())
.Build()
.RunAsync();
}

[Idempotent]
public static APIGatewayProxyResponse FunctionHandler(APIGatewayProxyRequest apigwProxyEvent,
ILambdaContext context)
{
return new APIGatewayProxyResponse
{
Body = JsonSerializer.Serialize(response, typeof(Response), LambdaFunctionJsonSerializerContext.Default),
StatusCode = 200,
Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
};
}
}

[JsonSerializable(typeof(APIGatewayProxyRequest))]
[JsonSerializable(typeof(APIGatewayProxyResponse))]
[JsonSerializable(typeof(Response))]
public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext
{
}
```

## Testing your code

The idempotency utility provides several routes to test your code.

You can check our Integration tests which use [TestContainers](https://testcontainers.com/modules/dynamodb/){:target="_blank"} with a local DynamoDB instance to test the idempotency utility. Or our end-to-end tests which use the AWS SDK to interact with a real DynamoDB table.

### Disabling the idempotency utility
When testing your code, you may wish to disable the idempotency logic altogether and focus on testing your business logic. To do this, you can set the environment variable `POWERTOOLS_IDEMPOTENCY_DISABLED` to true.

Expand Down
58 changes: 44 additions & 14 deletions libraries/AWS.Lambda.Powertools.sln
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Function", "tests\e2e\funct
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Function.Tests", "tests\e2e\functions\idempotency\Function\test\Function.Tests\Function.Tests.csproj", "{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-Function", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-Function\AOT-Function.csproj", "{56DFC68A-3994-43CD-A17C-323495F1709C}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionPayloadSubsetTest", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-FunctionPayloadSubsetTest\AOT-FunctionPayloadSubsetTest.csproj", "{ACA789EA-BD38-490B-A7F8-6A3A86985025}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionHandlerTest", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-FunctionHandlerTest\AOT-FunctionHandlerTest.csproj", "{E71C48D2-AD56-4177-BBD7-6BB859A40C92}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOT-FunctionMethodAttributeTest", "tests\e2e\functions\idempotency\AOT-Function\src\AOT-FunctionMethodAttributeTest\AOT-FunctionMethodAttributeTest.csproj", "{CC8CFF43-DC72-464C-A42D-55E023DE8500}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -478,18 +482,42 @@ Global
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}.Release|x64.Build.0 = Release|Any CPU
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}.Release|x86.ActiveCfg = Release|Any CPU
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC}.Release|x86.Build.0 = Release|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x64.ActiveCfg = Debug|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x64.Build.0 = Debug|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x86.ActiveCfg = Debug|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Debug|x86.Build.0 = Debug|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|Any CPU.Build.0 = Release|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x64.ActiveCfg = Release|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x64.Build.0 = Release|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x86.ActiveCfg = Release|Any CPU
{56DFC68A-3994-43CD-A17C-323495F1709C}.Release|x86.Build.0 = Release|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x64.ActiveCfg = Debug|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x64.Build.0 = Debug|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x86.ActiveCfg = Debug|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Debug|x86.Build.0 = Debug|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|Any CPU.Build.0 = Release|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x64.ActiveCfg = Release|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x64.Build.0 = Release|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x86.ActiveCfg = Release|Any CPU
{ACA789EA-BD38-490B-A7F8-6A3A86985025}.Release|x86.Build.0 = Release|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x64.ActiveCfg = Debug|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x64.Build.0 = Debug|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x86.ActiveCfg = Debug|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Debug|x86.Build.0 = Debug|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|Any CPU.Build.0 = Release|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x64.ActiveCfg = Release|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x64.Build.0 = Release|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x86.ActiveCfg = Release|Any CPU
{E71C48D2-AD56-4177-BBD7-6BB859A40C92}.Release|x86.Build.0 = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x64.ActiveCfg = Debug|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x64.Build.0 = Debug|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x86.ActiveCfg = Debug|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Debug|x86.Build.0 = Debug|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|Any CPU.Build.0 = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x64.ActiveCfg = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x64.Build.0 = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x86.ActiveCfg = Release|Any CPU
{CC8CFF43-DC72-464C-A42D-55E023DE8500}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection

GlobalSection(NestedProjects) = preSolution
Expand Down Expand Up @@ -532,6 +560,8 @@ Global
{FB2C7DA3-6FCE-429D-86F9-5775D0231EC6} = {CDAE55EB-9438-4F54-B7ED-931D64324D5F}
{9AF99F6D-E8E7-443F-A965-D55B8E388836} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{FBCE2C8A-2F64-4B62-8CF1-D4A14C19A5CC} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{56DFC68A-3994-43CD-A17C-323495F1709C} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{ACA789EA-BD38-490B-A7F8-6A3A86985025} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{E71C48D2-AD56-4177-BBD7-6BB859A40C92} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
{CC8CFF43-DC72-464C-A42D-55E023DE8500} = {FB2C7DA3-6FCE-429D-86F9-5775D0231EC6}
EndGlobalSection
EndGlobal
28 changes: 22 additions & 6 deletions libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
*
* http://aws.amazon.com/apache2.0
*
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

using System;
using System.Text.Json.Serialization;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
using AWS.Lambda.Powertools.Idempotency.Persistence;

namespace AWS.Lambda.Powertools.Idempotency;
Expand All @@ -27,7 +29,7 @@ namespace AWS.Lambda.Powertools.Idempotency;
/// Use it before the function handler get called.
/// Example: Idempotency.Configure(builder => builder.WithPersistenceStore(...));
/// </summary>
public sealed class Idempotency
public sealed class Idempotency
{
/// <summary>
/// The general configurations for the idempotency
Expand All @@ -47,6 +49,7 @@ internal Idempotency(IPowertoolsConfigurations powertoolsConfigurations)
{
powertoolsConfigurations.SetExecutionEnvironment(this);
}

/// <summary>
/// Set Idempotency options
/// </summary>
Expand All @@ -68,7 +71,7 @@ private void SetPersistenceStore(BasePersistenceStore persistenceStore)
/// <summary>
/// Holds the idempotency Instance:
/// </summary>
public static Idempotency Instance { get; } = new(PowertoolsConfigurations.Instance);
internal static Idempotency Instance { get; } = new(PowertoolsConfigurations.Instance);

/// <summary>
/// Use this method to configure persistence layer (mandatory) and idempotency options (optional)
Expand All @@ -90,7 +93,7 @@ public static void Configure(Action<IdempotencyBuilder> configurationAction)
/// Holds ILambdaContext
/// </summary>
public ILambdaContext LambdaContext { get; private set; }

/// <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.
Expand Down Expand Up @@ -177,5 +180,18 @@ public IdempotencyBuilder WithOptions(IdempotencyOptions options)
Options = options;
return this;
}

#if NET8_0_OR_GREATER
/// <summary>
/// Set Customer JsonSerializerContext to append to IdempotencySerializationContext
/// </summary>
/// <param name="context"></param>
/// <returns>IdempotencyBuilder</returns>
public IdempotencyBuilder WithJsonSerializationContext(JsonSerializerContext context)
{
IdempotencySerializer.AddTypeInfoResolver(context);
return this;
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,32 @@ public class IdempotencyOptionsBuilder
/// Default maximum number of items in the local cache.
/// </summary>
private readonly int _localCacheMaxItems = 256;

/// <summary>
/// Local cache enabled
/// </summary>
private bool _useLocalCache;

/// <summary>
/// Default expiration in seconds.
/// </summary>
private long _expirationInSeconds = 60 * 60; // 1 hour

/// <summary>
/// Event key JMESPath expression.
/// </summary>
private string _eventKeyJmesPath;

/// <summary>
/// Payload validation JMESPath expression.
/// </summary>
private string _payloadValidationJmesPath;

/// <summary>
/// Throw exception if no idempotency key is found.
/// </summary>
private bool _throwOnNoIdempotencyKey;

/// <summary>
/// Default Hash function
/// </summary>
Expand Down Expand Up @@ -107,7 +113,7 @@ public IdempotencyOptionsBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdemp
/// <returns>the instance of the builder (to chain operations)</returns>
public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration)
{
_expirationInSeconds = (long) duration.TotalSeconds;
_expirationInSeconds = (long)duration.TotalSeconds;
return this;
}

Expand All @@ -116,9 +122,15 @@ public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration)
/// </summary>
/// <param name="hashFunction">Can be any algorithm supported by HashAlgorithm.Create</param>
/// <returns>the instance of the builder (to chain operations)</returns>
#if NET8_0_OR_GREATER
[Obsolete("Idempotency uses MD5 and does not support other hash algorithms.")]
#endif
Comment on lines +125 to +127
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I think make sense we add this in other runtime because we don't allow customer to bring others hash algorithm.

public IdempotencyOptionsBuilder WithHashFunction(string hashFunction)
{
#if NET6_0
// for backward compability keep this code in .net 6
_hashFunction = hashFunction;
#endif
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using AWS.Lambda.Powertools.Common;
using AWS.Lambda.Powertools.Idempotency.Exceptions;
using AWS.Lambda.Powertools.Idempotency.Internal;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;

namespace AWS.Lambda.Powertools.Idempotency;

Expand Down Expand Up @@ -151,7 +152,7 @@ private static JsonDocument GetPayload<T>(AspectEventArgs eventArgs)
// Use the first argument if IdempotentAttribute placed on handler or number of arguments is 1
if (isPlacedOnRequestHandler || args.Count == 1)
{
payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null;
payload = args is not null && args.Any() ? JsonDocument.Parse(IdempotencySerializer.Serialize(args[0], typeof(object))) : null;
}
else
{
Expand All @@ -160,7 +161,7 @@ private static JsonDocument GetPayload<T>(AspectEventArgs eventArgs)
if (parameter != null)
{
// set payload to the value of the parameter
payload = JsonDocument.Parse(JsonSerializer.Serialize(args[Array.IndexOf(eventArgsMethod.GetParameters(), parameter)]));
payload = JsonDocument.Parse(IdempotencySerializer.Serialize(args[Array.IndexOf(eventArgsMethod.GetParameters(), parameter)], typeof(object)));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using AWS.Lambda.Powertools.Idempotency.Exceptions;
using AWS.Lambda.Powertools.Idempotency.Internal.Serializers;
using AWS.Lambda.Powertools.Idempotency.Persistence;

namespace AWS.Lambda.Powertools.Idempotency.Internal;
Expand Down Expand Up @@ -184,7 +185,7 @@ private Task<T> HandleForStatus(DataRecord record)
default:
try
{
var result = JsonSerializer.Deserialize<T>(record.ResponseData!);
var result = IdempotencySerializer.Deserialize<T>(record.ResponseData!);
if (result is null)
{
throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/

using System.Text.Json.Serialization;

namespace AWS.Lambda.Powertools.Idempotency.Internal.Serializers;

#if NET8_0_OR_GREATER


/// <summary>
/// The source generated JsonSerializerContext to be used to Serialize Idempotency types
/// </summary>
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(object))]
public partial class IdempotencySerializationContext : JsonSerializerContext
{

}
#endif
Loading
Loading