From 337cf1e6b1dedef75c3b6e4de391679c09874810 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Tue, 23 Aug 2022 08:50:47 +0400 Subject: [PATCH 01/32] Add idempotency module to solution --- libraries/AWS.Lambda.Powertools.sln | 30 ++ .../AWS.Lambda.Powertools.Idempotency.csproj | 18 + .../Constants.cs | 22 + .../IdempotencyAlreadyInProgressException.cs | 31 ++ .../IdempotencyConfigurationException.cs | 31 ++ .../IdempotencyInconsistentStateException.cs | 31 ++ .../IdempotencyItemAlreadyExistsException.cs | 31 ++ .../IdempotencyItemNotFoundException.cs | 31 ++ .../Exceptions/IdempotencyKeyException.cs | 31 ++ .../IdempotencyPersistenceLayerException.cs | 31 ++ .../IdempotencyValidationException.cs | 31 ++ .../Idempotency.cs | 112 +++++ .../IdempotencyConfig.cs | 171 +++++++ .../IdempotentAttribute.cs | 25 + .../Internal/IdempotencyHandler.cs | 219 +++++++++ .../Internal/IdempotentAspect.cs | 67 +++ .../Internal/LRUCache.cs | 134 ++++++ .../InternalsVisibleTo.cs | 18 + .../Output/ConsoleLog.cs | 64 +++ .../Output/ILog.cs | 48 ++ .../Persistence/BasePersistenceStore.cs | 340 ++++++++++++++ .../Persistence/DataRecord.cs | 98 ++++ .../Persistence/DynamoDBPersistenceStore.cs | 371 +++++++++++++++ .../Persistence/IPersistenceStore.cs | 55 +++ .../Serialization/JsonFunction.cs | 36 ++ ...Lambda.Powertools.Idempotency.Tests.csproj | 44 ++ .../Handlers/IdempotencyEnabledFunction.cs | 36 ++ .../Handlers/IdempotencyFunction.cs | 101 +++++ .../Handlers/IdempotencyWithErrorFunction.cs | 28 ++ .../IdempotencyTest.cs | 57 +++ .../IntegrationTestBase.cs | 79 ++++ .../Internal/IdempotentAspectTests.cs | 179 ++++++++ .../Internal/LRUCacheTest.cs | 167 +++++++ .../Model/Basket.cs | 56 +++ .../Model/Product.cs | 55 +++ .../Persistence/BasePersistenceStoreTests.cs | 427 ++++++++++++++++++ .../Persistence/DataRecordTests.cs | 71 +++ .../DynamoDBPersistenceStoreTests.cs | 348 ++++++++++++++ .../resources/apigw_event.json | 62 +++ .../resources/apigw_event2.json | 62 +++ 40 files changed, 3848 insertions(+) create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/InternalsVisibleTo.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTest.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event.json create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event2.json diff --git a/libraries/AWS.Lambda.Powertools.sln b/libraries/AWS.Lambda.Powertools.sln index 9c5da435..ae37af0b 100644 --- a/libraries/AWS.Lambda.Powertools.sln +++ b/libraries/AWS.Lambda.Powertools.sln @@ -23,6 +23,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Traci EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Common.Tests", "tests\AWS.Lambda.Powertools.Common.Tests\AWS.Lambda.Powertools.Common.Tests.csproj", "{4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Idempotency", "src\AWS.Lambda.Powertools.Idempotency\AWS.Lambda.Powertools.Idempotency.csproj", "{B7AC87DF-9705-47D9-AC00-C230E577CA5D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AWS.Lambda.Powertools.Idempotency.Tests", "tests\AWS.Lambda.Powertools.Idempotency.Tests\AWS.Lambda.Powertools.Idempotency.Tests.csproj", "{3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -132,6 +136,30 @@ Global {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}.Release|x64.Build.0 = Release|Any CPU {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}.Release|x86.ActiveCfg = Release|Any CPU {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E}.Release|x86.Build.0 = Release|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Debug|x64.ActiveCfg = Debug|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Debug|x64.Build.0 = Debug|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Debug|x86.ActiveCfg = Debug|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Debug|x86.Build.0 = Debug|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Release|Any CPU.Build.0 = Release|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Release|x64.ActiveCfg = Release|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Release|x64.Build.0 = Release|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Release|x86.ActiveCfg = Release|Any CPU + {B7AC87DF-9705-47D9-AC00-C230E577CA5D}.Release|x86.Build.0 = Release|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Debug|x64.ActiveCfg = Debug|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Debug|x64.Build.0 = Debug|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Debug|x86.ActiveCfg = Debug|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Debug|x86.Build.0 = Debug|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Release|Any CPU.Build.0 = Release|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Release|x64.ActiveCfg = Release|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Release|x64.Build.0 = Release|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Release|x86.ActiveCfg = Release|Any CPU + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution @@ -143,5 +171,7 @@ Global {A422C742-2CF9-409D-BDAE-15825AB62113} = {1CFF5568-8486-475F-81F6-06105C437528} {4EC48E6A-45B5-4E25-ABBD-C23FE2BD6E1E} = {1CFF5568-8486-475F-81F6-06105C437528} {A040AED5-BBB8-4BFA-B2A5-BBD82817B8A5} = {1CFF5568-8486-475F-81F6-06105C437528} + {B7AC87DF-9705-47D9-AC00-C230E577CA5D} = {73C9B1E5-3893-47E8-B373-17E5F5D7E6F5} + {3E1D77BD-70AF-4767-B00A-4A321D5AB2C3} = {1CFF5568-8486-475F-81F6-06105C437528} EndGlobalSection EndGlobal diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj new file mode 100644 index 00000000..a9c56e61 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + 0.0.1 + + + + + + + + + + + diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs new file mode 100644 index 00000000..61dd97c7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs @@ -0,0 +1,22 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency; + +public class Constants { + public static readonly string LAMBDA_FUNCTION_NAME_ENV = "AWS_LAMBDA_FUNCTION_NAME"; + public static readonly string AWS_REGION_ENV = "AWS_REGION"; + public static readonly string IDEMPOTENCY_DISABLED_ENV = "POWERTOOLS_IDEMPOTENCY_DISABLED"; +} diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs new file mode 100644 index 00000000..7d8aefa2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyAlreadyInProgressException: Exception +{ + public IdempotencyAlreadyInProgressException() + { + } + + public IdempotencyAlreadyInProgressException(string? message) : base(message) + { + } + + public IdempotencyAlreadyInProgressException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs new file mode 100644 index 00000000..ccb838d9 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyConfigurationException : Exception +{ + public IdempotencyConfigurationException() + { + } + + public IdempotencyConfigurationException(string? message) : base(message) + { + } + + public IdempotencyConfigurationException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs new file mode 100644 index 00000000..5d3257c9 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyInconsistentStateException : Exception +{ + public IdempotencyInconsistentStateException() + { + } + + public IdempotencyInconsistentStateException(string? message) : base(message) + { + } + + public IdempotencyInconsistentStateException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs new file mode 100644 index 00000000..d4fe7de0 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyItemAlreadyExistsException : Exception +{ + public IdempotencyItemAlreadyExistsException() + { + } + + public IdempotencyItemAlreadyExistsException(string? message) : base(message) + { + } + + public IdempotencyItemAlreadyExistsException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs new file mode 100644 index 00000000..f1cdbd0a --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyItemNotFoundException : Exception +{ + public IdempotencyItemNotFoundException() + { + } + + public IdempotencyItemNotFoundException(string? message) : base(message) + { + } + + public IdempotencyItemNotFoundException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs new file mode 100644 index 00000000..6c57dcea --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyKeyException : Exception +{ + public IdempotencyKeyException() + { + } + + public IdempotencyKeyException(string? message) : base(message) + { + } + + public IdempotencyKeyException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs new file mode 100644 index 00000000..49bc25eb --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyPersistenceLayerException : Exception +{ + public IdempotencyPersistenceLayerException() + { + } + + public IdempotencyPersistenceLayerException(string? message) : base(message) + { + } + + public IdempotencyPersistenceLayerException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs new file mode 100644 index 00000000..e4a3aea2 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Exceptions; + +public class IdempotencyValidationException : Exception +{ + public IdempotencyValidationException() + { + } + + public IdempotencyValidationException(string? message) : base(message) + { + } + + public IdempotencyValidationException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs new file mode 100644 index 00000000..d7ffa44e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -0,0 +1,112 @@ +/* + * 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 AWS.Lambda.Powertools.Idempotency.Persistence; + +namespace AWS.Lambda.Powertools.Idempotency; + +/// +/// Holds the configuration for idempotency: +/// The persistence layer to use for persisting the request and response of the function (mandatory). +/// The general configuration for idempotency (optional, see {@link IdempotencyConfig.Builder} methods to see defaults values. +/// Use it before the function handler get called. +/// Example: Idempotency.Config().WithPersistenceStore(...).Configure(); +/// +public class Idempotency +{ + private IdempotencyConfig _idempotencyConfig; + private BasePersistenceStore _persistenceStore; + + private Idempotency() + { + } + + public IdempotencyConfig GetIdempotencyConfig() + { + return _idempotencyConfig; + } + + public BasePersistenceStore GetPersistenceStore() + { + if (_persistenceStore == null) + { + throw new NullReferenceException("Persistence Store is null, did you call 'Configure()'?"); + } + return _persistenceStore; + } + + private void SetConfig(IdempotencyConfig config) + { + _idempotencyConfig = config; + } + + private void SetPersistenceStore(BasePersistenceStore persistenceStore) + { + _persistenceStore = persistenceStore; + } + + private static class Holder { + public static readonly Idempotency IdempotencyInstance = new Idempotency(); + } + + public static Idempotency Instance() => Holder.IdempotencyInstance; + + /// + /// Acts like a builder that can be used to configure Idempotency + /// + /// + public static IdempotencyBuilder Config() + { + return new IdempotencyBuilder(); + } + + public class IdempotencyBuilder { + + private IdempotencyConfig _config; + private BasePersistenceStore _store; + + /// + /// Use this method after configuring persistence layer (mandatory) and idem potency configuration (optional) + /// + /// + public void Configure() + { + if (_store == null) + { + throw new NullReferenceException("Persistence Layer is null, configure one with 'WithPersistenceStore()'"); + } + if (_config == null) + { + _config = IdempotencyConfig.Builder().Build(); + } + Idempotency.Instance().SetConfig(_config); + Idempotency.Instance().SetPersistenceStore(_store); + } + + public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceStore) + { + this._store = persistenceStore; + return this; + } + + public IdempotencyBuilder WithConfig(IdempotencyConfig config) + { + this._config = config; + return this; + } + } + + +} diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs new file mode 100644 index 00000000..3495b5df --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs @@ -0,0 +1,171 @@ +/* + * 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 AWS.Lambda.Powertools.Idempotency.Output; + +namespace AWS.Lambda.Powertools.Idempotency; + +/// +/// Configuration of the idempotency feature. Use the Builder to create an instance. +/// +public class IdempotencyConfig +{ + public string EventKeyJmesPath { get; } + public string PayloadValidationJmesPath { get; } + public bool ThrowOnNoIdempotencyKey { get; } + public bool UseLocalCache { get; } + public int LocalCacheMaxItems { get; } + public long ExpirationInSeconds { get; } + public string HashFunction { get; } + public ILog Log { get; } + + private IdempotencyConfig( + string eventKeyJmesPath, + string payloadValidationJmesPath, + bool throwOnNoIdempotencyKey, + bool useLocalCache, + int localCacheMaxItems, + long expirationInSeconds, + string hashFunction, + ILog log) + { + EventKeyJmesPath = eventKeyJmesPath; + PayloadValidationJmesPath = payloadValidationJmesPath; + ThrowOnNoIdempotencyKey = throwOnNoIdempotencyKey; + UseLocalCache = useLocalCache; + LocalCacheMaxItems = localCacheMaxItems; + ExpirationInSeconds = expirationInSeconds; + HashFunction = hashFunction; + Log = log; + } + + public static IdempotencyConfigBuilder Builder() + { + return new IdempotencyConfigBuilder(); + } + + public class IdempotencyConfigBuilder + { + private int _localCacheMaxItems = 256; + private bool _useLocalCache = false; + private long _expirationInSeconds = 60 * 60; // 1 hour + private string _eventKeyJmesPath = null; + private string _payloadValidationJmesPath; + private bool _throwOnNoIdempotencyKey = false; + private string _hashFunction = "MD5"; + private ILog _log = new ConsoleLog(); + + /// + /// Initialize and return an instance of IdempotencyConfig. + /// Example: + /// IdempotencyConfig.Builder().WithUseLocalCache().Build(); + /// This instance must then be passed to the Idempotency.Config: + /// Idempotency.Config().WithConfig(config).Configure(); + /// + /// an instance of IdempotencyConfig + public IdempotencyConfig Build() => + new IdempotencyConfig(_eventKeyJmesPath, + _payloadValidationJmesPath, + _throwOnNoIdempotencyKey, + _useLocalCache, + _localCacheMaxItems, + _expirationInSeconds, + _hashFunction, + _log); + + /// + /// A JMESPath expression to extract the idempotency key from the event record. + /// See https://jmespath.org/ for more details. + /// + /// path of the key in the Lambda event + /// the instance of the builder (to chain operations) + public IdempotencyConfigBuilder WithEventKeyJmesPath(string eventKeyJmesPath) + { + _eventKeyJmesPath = eventKeyJmesPath; + return this; + } + + /// + /// Whether to locally cache idempotency results, by default false + /// + /// Indicate if a local cache must be used in addition to the persistence store. + /// the instance of the builder (to chain operations) + public IdempotencyConfigBuilder WithUseLocalCache(bool useLocalCache) + { + _useLocalCache = useLocalCache; + return this; + } + + /// + /// A JMESPath expression to extract the payload to be validated from the event record. + /// See https://jmespath.org/ for more details. + /// + /// JMES Path of a part of the payload to be used for validation + /// the instance of the builder (to chain operations) + public IdempotencyConfigBuilder WithPayloadValidationJmesPath(string payloadValidationJmesPath) + { + _payloadValidationJmesPath = payloadValidationJmesPath; + return this; + } + + /// + /// Whether to throw an exception if no idempotency key was found in the request, by default false + /// + /// boolean to indicate if we must throw an Exception when + /// idempotency key could not be found in the payload. + /// the instance of the builder (to chain operations) + public IdempotencyConfigBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdempotencyKey) + { + _throwOnNoIdempotencyKey = throwOnNoIdempotencyKey; + return this; + } + + /// + /// The number of seconds to wait before a record is expired + /// + /// expiration of the record in the store + /// the instance of the builder (to chain operations) + public IdempotencyConfigBuilder WithExpiration(TimeSpan duration) + { + _expirationInSeconds = (long)duration.TotalSeconds; + return this; + } + + /// + /// Function to use for calculating hashes, by default MD5. + /// + /// Can be any algorithm supported by HashAlgorithm.Create + /// the instance of the builder (to chain operations) + public IdempotencyConfigBuilder WithHashFunction(string hashFunction) + { + _hashFunction = hashFunction; + return this; + } + + /// + /// Logs to a custom logger. + /// + /// + /// The logger. + /// + /// The same builder + /// + public IdempotencyConfigBuilder LogTo(ILog log) + { + _log = log; + return this; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs new file mode 100644 index 00000000..9bbe09a9 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -0,0 +1,25 @@ +/* + * 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 AspectInjector.Broker; +using AWS.Lambda.Powertools.Idempotency.Internal; + +namespace AWS.Lambda.Powertools.Idempotency; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +[Injection(typeof(IdempotentAspect), Inherited = true)] +public class IdempotentAttribute : Attribute +{ +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs new file mode 100644 index 00000000..c5ad3c16 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -0,0 +1,219 @@ +/* + * 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 AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Output; +using AWS.Lambda.Powertools.Idempotency.Persistence; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AWS.Lambda.Powertools.Idempotency.Internal; + +public class IdempotencyHandler +{ + private static readonly int MAX_RETRIES = 2; + + private readonly Func _target; + private readonly object[] _args; + private readonly JToken _data; + private readonly BasePersistenceStore _persistenceStore; + private readonly ILog _log; + + public IdempotencyHandler( + Func target, + object[] args, + string functionName, + JToken payload) + { + _target = target; + _args = args; + _data = payload; + _persistenceStore = Idempotency.Instance().GetPersistenceStore(); + _persistenceStore.Configure(Idempotency.Instance().GetIdempotencyConfig(), functionName); + _log = Idempotency.Instance().GetIdempotencyConfig().Log; + } + + /// + /// Main entry point for handling idempotent execution of a function. + /// + /// function response + public async Task Handle() + { + // IdempotencyInconsistentStateException can happen under rare but expected cases + // when persistent state changes in the small time between put & get requests. + // In most cases we can retry successfully on this exception. + for (int i = 0; true; i++) + { + try + { + var processIdempotency = await ProcessIdempotency(); + return processIdempotency; + } + catch (IdempotencyInconsistentStateException) + { + if (i == MAX_RETRIES) + { + throw; + } + } + } + } + + /// + /// Process the function with idempotency + /// + /// function response + /// + private async Task ProcessIdempotency() + { + + try + { + // We call saveInProgress first as an optimization for the most common case where no idempotent record + // already exists. If it succeeds, there's no need to call getRecord. + await _persistenceStore.SaveInProgress(_data, DateTimeOffset.UtcNow); + } + catch (IdempotencyItemAlreadyExistsException) + { + DataRecord record = await GetIdempotencyRecord(); + return await HandleForStatus(record); + } + catch (IdempotencyKeyException) + { + throw; + } + catch (Exception e) + { + throw new IdempotencyPersistenceLayerException( + "Failed to save in progress record to idempotency store. If you believe this is a powertools bug, please open an issue.", + e); + } + + var result = await GetFunctionResponse(); + return result; + } + + /// + /// Retrieve the idempotency record from the persistence layer. + /// + /// the record if available + /// + /// + private Task GetIdempotencyRecord() + { + try + { + return _persistenceStore.GetRecord(_data, DateTimeOffset.UtcNow); + } + catch (IdempotencyItemNotFoundException e) + { + // This code path will only be triggered if the record is removed between saveInProgress and getRecord + _log.WriteDebug("An existing idempotency record was deleted before we could fetch it"); + throw new IdempotencyInconsistentStateException("saveInProgress and getRecord return inconsistent results", + e); + } + catch (IdempotencyValidationException) + { + throw; + } + catch (IdempotencyKeyException) + { + throw; + } + catch (Exception e) + { + throw new IdempotencyPersistenceLayerException( + "Failed to get record from idempotency store. If you believe this is a powertools bug, please open an issue.", + e); + } + } + + /// + /// Take appropriate action based on data_record's status + /// + /// record DataRecord + /// Function's response previously used for this idempotency key, if it has successfully executed already. + /// + /// + /// + private Task HandleForStatus(DataRecord record) + { + // This code path will only be triggered if the record becomes expired between the saveInProgress call and here + if (DataRecord.DataRecordStatus.EXPIRED == record.Status) + { + throw new IdempotencyInconsistentStateException("saveInProgress and getRecord return inconsistent results"); + } + + if (DataRecord.DataRecordStatus.INPROGRESS == record.Status) + { + throw new IdempotencyAlreadyInProgressException("Execution already in progress with idempotency key: " + + record.IdempotencyKey); + } + + try + { + _log.WriteDebug("Response for key '{0}' retrieved from idempotency store, skipping the function", record.IdempotencyKey); + var result = JsonConvert.DeserializeObject(record.ResponseData); + return Task.FromResult(result); + } + catch (Exception e) + { + throw new IdempotencyPersistenceLayerException("Unable to get function response as " + typeof(T).Name, e); + } + } + + private async Task GetFunctionResponse() + { + T response; + try + { + response = await (Task)_target(_args); + } + catch(Exception handlerException) + { + // We need these nested blocks to preserve function's exception in case the persistence store operation + // also raises an exception + try + { + await _persistenceStore.DeleteRecord(_data, handlerException); + } + catch (IdempotencyKeyException) + { + throw; + } + catch (Exception e) + { + throw new IdempotencyPersistenceLayerException( + "Failed to delete record from idempotency store. If you believe this is a powertools bug, please open an issue.", + e); + } + + throw; + } + + try + { + await _persistenceStore.SaveSuccess(_data, response, DateTimeOffset.UtcNow); + } + catch (Exception e) + { + throw new IdempotencyPersistenceLayerException( + "Failed to update record state to success in idempotency store. If you believe this is a powertools bug, please open an issue.", + e); + } + + return response; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs new file mode 100644 index 00000000..d7be68b8 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs @@ -0,0 +1,67 @@ +/* + * 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.Reflection; +using AspectInjector.Broker; +using AWS.Lambda.Powertools.Idempotency.Exceptions; +using Newtonsoft.Json.Linq; + +namespace AWS.Lambda.Powertools.Idempotency.Internal; + +[Aspect(Scope.Global)] +public class IdempotentAspect +{ + private static MethodInfo _asyncErrorHandler = typeof(IdempotentAspect).GetMethod(nameof(WrapAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + + [Advice(Kind.Around, Targets = Target.Method)] + public object Handle( + [Argument(Source.Target)] Func target, + [Argument(Source.Arguments)] object[] args, + [Argument(Source.Instance)] object instance, + [Argument(Source.ReturnType)] Type retType, + [Argument(Source.Triggers)] Attribute[] triggers, + [Argument(Source.Metadata)] MethodBase method + ) + { + if (!typeof(Task).IsAssignableFrom((Type?) retType)) + { + throw new IdempotencyConfigurationException("Invalid Target Exception"); + } + + var syncResultType = retType.IsConstructedGenericType ? retType.GenericTypeArguments[0] : typeof(Task); + return _asyncErrorHandler.MakeGenericMethod(syncResultType).Invoke(this, new object[] { target, args, method }); + } + + private static async Task WrapAsync(Func target, object[] args, MethodBase method) + { + string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV); + if (idempotencyDisabledEnv is "true") + { + return await (Task)target(args); + } + JToken payload = JToken.FromObject(args[0]); + if (payload == null) + { + throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); + } + + var types = new[] {typeof(T)}; + var genericType = typeof(IdempotencyHandler<>).MakeGenericType(types); + var idempotencyHandler = (IdempotencyHandler)Activator.CreateInstance(genericType,target, args, method.Name, payload); + var result = await idempotencyHandler.Handle(); + return result; + + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs new file mode 100644 index 00000000..c7b769c1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs @@ -0,0 +1,134 @@ + +#nullable disable + +namespace AWS.Lambda.Powertools.Idempotency.Internal; +//source: https://github.dev/microsoft/botbuilder-dotnet/blob/main/libraries/AdaptiveExpressions/LRUCache.cs + +/// +/// A least-recently-used cache stored like a dictionary. +/// +/// The type of the key to the cached item. +/// The type of the cached item. +internal sealed class LRUCache +{ + /// + /// Default maximum number of elements to cache. + /// + private const int DefaultCapacity = 255; + + private readonly object _lockObj = new object(); + private readonly int _capacity; + private readonly Dictionary _cacheMap; + private readonly LinkedList _cacheList; + + /// + /// Initializes a new instance of the class. + /// + public LRUCache() + : this(DefaultCapacity) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Maximum number of elements to cache. + public LRUCache(int capacity) + { + _capacity = capacity > 0 ? capacity : DefaultCapacity; + _cacheMap = new Dictionary(); + _cacheList = new LinkedList(); + } + + /// + /// Gets the value associated with the specified key. + /// + /// The key of the value to get. + /// When this method returns, contains the value associated with + /// the specified key, if the key is found; otherwise, the default value for the + /// type of the parameter. + /// true if contains an element with the specified key; otherwise, false. + public bool TryGet(TKey key, out TValue value) + { + lock (_lockObj) + { + if (_cacheMap.TryGetValue(key, out var entry)) + { + Touch(entry.Node); + value = entry.Value; + return true; + } + } + + value = default(TValue); + return false; + } + + /// + /// Adds the specified key and value to the cache. + /// + /// The key of the element to add. + /// The value of the element to add. + public void Set(TKey key, TValue value) + { + lock (_lockObj) + { + if (!_cacheMap.TryGetValue(key, out var entry)) + { + LinkedListNode node; + if (_cacheMap.Count >= _capacity) + { + node = _cacheList.Last; + _cacheMap.Remove(node.Value); + _cacheList.RemoveLast(); + node.Value = key; + } + else + { + node = new LinkedListNode(key); + } + + _cacheList.AddFirst(node); + _cacheMap.Add(key, new Entry(node, value)); + } + else + { + entry.Value = value; + _cacheMap[key] = entry; + Touch(entry.Node); + } + } + } + + public void Delete(TKey key) + { + lock (_lockObj) + { + _cacheList.Remove(key); + _cacheMap.Remove(key); + } + } + + public int Count => _cacheList.Count; + + private void Touch(LinkedListNode node) + { + if (node != _cacheList.First) + { + _cacheList.Remove(node); + _cacheList.AddFirst(node); + } + } + + private struct Entry + { + public LinkedListNode Node; + public TValue Value; + + public Entry(LinkedListNode node, TValue value) + { + this.Node = node; + this.Value = value; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/InternalsVisibleTo.cs new file mode 100644 index 00000000..3d73602c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/InternalsVisibleTo.cs @@ -0,0 +1,18 @@ +/* + * 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.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Idempotency.Tests")] \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs new file mode 100644 index 00000000..1bffc78c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs @@ -0,0 +1,64 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Output; + +/// +/// A log that writes to the console in a colorful way. +/// +public class ConsoleLog : ILog +{ + /// + /// Writes an informational message to the log. + /// + /// The format. + /// The args. + public void WriteInformation(string format, params object[] args) => Write(ConsoleColor.White, format, args); + + /// + /// Writes an error message to the log. + /// + /// The format. + /// The args. + public void WriteError(string format, params object[] args) => Write(ConsoleColor.Red, format, args); + + /// + /// Writes a warning message to the log. + /// + /// The format. + /// The args. + public void WriteWarning(string format, params object[] args) => Write(ConsoleColor.Yellow, format, args); + + /// + /// Writes a debug message to the log. + /// + /// The format. + /// The args. + public void WriteDebug(string format, params object[] args) => Write(ConsoleColor.Cyan, format, args); + + static void Write(ConsoleColor color, string format, object[] args) + { + var oldColor = Console.ForegroundColor; + Console.ForegroundColor = color; + try + { + Console.WriteLine(format, args); + } + finally + { + Console.ForegroundColor = oldColor; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs new file mode 100644 index 00000000..8696fc07 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs @@ -0,0 +1,48 @@ +/* + * 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. + */ + +//TODO: Replace with core PowerTools logging +namespace AWS.Lambda.Powertools.Idempotency.Output; + +public interface ILog +{ + /// + /// Writes an informational message to the log. + /// + /// The format. + /// The args. + void WriteInformation(string format, params object[] args); + + /// + /// Writes an error message to the log. + /// + /// The format. + /// The args. + void WriteError(string format, params object[] args); + + /// + /// Writes a warning message to the log. + /// + /// The format. + /// The args. + void WriteWarning(string format, params object[] args); + + /// + /// Writes a debug message to the log. + /// + /// The format. + /// The args. + void WriteDebug(string format, params object[] args); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs new file mode 100644 index 00000000..483e3a3c --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -0,0 +1,340 @@ +/* + * 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.Security.Cryptography; +using System.Text; +using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Internal; +using AWS.Lambda.Powertools.Idempotency.Output; +using AWS.Lambda.Powertools.Idempotency.Serialization; +using DevLab.JmesPath; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace AWS.Lambda.Powertools.Idempotency.Persistence; + +public abstract class BasePersistenceStore : IPersistenceStore +{ + private IdempotencyConfig _idempotencyConfig; + private string? _functionName; + protected bool PayloadValidationEnabled; + private LRUCache _cache = null!; + protected ILog Log; + + public void Configure(IdempotencyConfig idempotencyConfig, string? functionName) + { + string? funcEnv = Environment.GetEnvironmentVariable(Constants.LAMBDA_FUNCTION_NAME_ENV); + _functionName = funcEnv != null ? funcEnv : "testFunction"; + if (!string.IsNullOrWhiteSpace(functionName)) + { + _functionName += "." + functionName; + } + _idempotencyConfig = idempotencyConfig; + Log = _idempotencyConfig.Log; + + //TODO: optimize to not reconfigure + if (!string.IsNullOrWhiteSpace(_idempotencyConfig.PayloadValidationJmesPath)) + { + PayloadValidationEnabled = true; + } + + var useLocalCache = _idempotencyConfig.UseLocalCache; + if (useLocalCache) + { + _cache = new (_idempotencyConfig.LocalCacheMaxItems); + } + } + + /// + /// For test purpose only (adding a cache to mock) + /// + internal void Configure(IdempotencyConfig config, string functionName, LRUCache cache) + { + Configure(config, functionName); + _cache = cache; + } + + public virtual async Task SaveSuccess(JToken data, object result, DateTimeOffset now) + { + string responseJson = JsonConvert.SerializeObject(result); + var record = new DataRecord( + GetHashedIdempotencyKey(data), + DataRecord.DataRecordStatus.COMPLETED, + GetExpiryEpochSecond(now), + responseJson, + GetHashedPayload(data) + ); + Log.WriteDebug("Function successfully executed. Saving record to persistence store with idempotency key: {0}", record.IdempotencyKey); + await UpdateRecord(record); + SaveToCache(record); + } + + public virtual async Task SaveInProgress(JToken data, DateTimeOffset now) + { + string idempotencyKey = GetHashedIdempotencyKey(data); + + if (RetrieveFromCache(idempotencyKey, now) != null) + { + throw new IdempotencyItemAlreadyExistsException(); + } + + DataRecord record = new DataRecord( + idempotencyKey, + DataRecord.DataRecordStatus.INPROGRESS, + GetExpiryEpochSecond(now), + null, + GetHashedPayload(data) + ); + Log.WriteDebug("saving in progress record for idempotency key: {0}", record.IdempotencyKey); + await PutRecord(record, now); + } + + /// + /// Delete record from the persistence store + /// + /// Payload + /// The throwable thrown by the function + public virtual async Task DeleteRecord(JToken data, Exception throwable) + { + string idemPotencyKey = GetHashedIdempotencyKey(data); + + Log.WriteDebug("Function raised an exception {0}. " + + "Clearing in progress record in persistence store for idempotency key: {1}", + throwable.GetType().Name, + idemPotencyKey); + + await DeleteRecord(idemPotencyKey); + DeleteFromCache(idemPotencyKey); + } + + /// + /// Retrieve idempotency key for data provided, fetch from persistence store, and convert to DataRecord. + /// + /// Payload + /// + /// DataRecord representation of existing record found in persistence store + public virtual async Task GetRecord(JToken data, DateTimeOffset now) + { + string idempotencyKey = GetHashedIdempotencyKey(data); + + DataRecord? cachedRecord = RetrieveFromCache(idempotencyKey, now); + if (cachedRecord != null) + { + Log.WriteDebug("Idempotency record found in cache with idempotency key: {0}", idempotencyKey); + ValidatePayload(data, cachedRecord); + return cachedRecord; + } + + DataRecord record = await GetRecord(idempotencyKey); + SaveToCache(record); + ValidatePayload(data, record); + return record; + } + + /// + /// Save data_record to local cache except when status is "INPROGRESS" + /// NOTE: We can't cache "INPROGRESS" records as we have no way to reflect updates that can happen outside of the + /// execution environment + /// + /// DataRecord to save in cache + private void SaveToCache(DataRecord dataRecord) + { + if (!_idempotencyConfig.UseLocalCache) + return; + if (dataRecord.Status == DataRecord.DataRecordStatus.INPROGRESS) + return; + + _cache.Set(dataRecord.IdempotencyKey, dataRecord); + } + + /// + /// Validate that the hashed payload matches data provided and stored data record + /// + /// Payload + /// DataRecord instance + /// + private void ValidatePayload(JToken data, DataRecord dataRecord) + { + if (PayloadValidationEnabled) + { + string dataHash = GetHashedPayload(data); + + if (dataHash != dataRecord.PayloadHash) + { + throw new IdempotencyValidationException("Payload does not match stored record for this event key"); + } + } + } + + private DataRecord? RetrieveFromCache(string idempotencyKey, DateTimeOffset now) + { + if (!_idempotencyConfig.UseLocalCache) + return null; + + if (_cache.TryGet(idempotencyKey, out DataRecord record) && record!=null) + { + if (!record.IsExpired(now)) + { + return record; + } + Log.WriteDebug("Removing expired local cache record for idempotency key: {0}", idempotencyKey); + DeleteFromCache(idempotencyKey); + } + return null; + } + private void DeleteFromCache(string idempotencyKey) + { + if (!_idempotencyConfig.UseLocalCache) + return; + + _cache.Delete(idempotencyKey); + } + + /// + /// Extract payload using validation key jmespath and return a hashed representation + /// + /// Payload + /// Hashed representation of the data extracted by the jmespath expression + private string GetHashedPayload(JToken data) + { + if (!PayloadValidationEnabled) + { + return ""; + } + + var jmes = new JmesPath(); + jmes.FunctionRepository.Register(); + var result = jmes.Transform(data.ToString(), _idempotencyConfig.PayloadValidationJmesPath); + var node = JToken.Parse(result); + return GenerateHash(node); + } + + + + /// + /// Calculate unix timestamp of expiry date for idempotency record + /// + /// + /// unix timestamp of expiry date for idempotency record + private long GetExpiryEpochSecond(DateTimeOffset now) + { + return now.AddSeconds(_idempotencyConfig.ExpirationInSeconds).ToUnixTimeSeconds(); + } + + /// + /// Extract idempotency key and return a hashed representation + /// + /// incoming data + /// Hashed representation of the data extracted by the jmespath expression + /// + private string GetHashedIdempotencyKey(JToken data) + { + JToken node = data; + var eventKeyJmesPath = _idempotencyConfig.EventKeyJmesPath; + if (eventKeyJmesPath != null) + { + var jmes = new JmesPath(); + jmes.FunctionRepository.Register(); + var result = jmes.Transform(data.ToString(), eventKeyJmesPath); + node = JToken.Parse(result); + } + + if (IsMissingIdemPotencyKey(node)) + { + if (_idempotencyConfig.ThrowOnNoIdempotencyKey) + { + throw new IdempotencyKeyException("No data found to create a hashed idempotency key"); + } + Log.WriteWarning("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyConfig.EventKeyJmesPath); + } + + string hash = GenerateHash(node); + return _functionName + "#" + hash; + } + + private bool IsMissingIdemPotencyKey(JToken data) + { + return (data == null) || + (data.Type == JTokenType.Array && !data.HasValues) || + (data.Type == JTokenType.Object && !data.HasValues) || + (data.Type == JTokenType.String && data.ToString() == String.Empty) || + (data.Type == JTokenType.Null); + } + + internal string GenerateHash(JToken data) + { + JToken node; + if (data is JObject or JArray) // if array or object, use the json string representation, otherwise get the real value + { + node = data; + } + else if (data.Type == JTokenType.String) + { + node = data.Value(); + } + else if (data.Type == JTokenType.Integer) + { + node = data.Value(); + } + else if (data.Type == JTokenType.Float) + { + node = data.Value(); + } + else if (data.Type == JTokenType.Boolean) + { + node = data.Value(); + } + else node = data; // anything else + + + using var hashAlgorithm = HashAlgorithm.Create(_idempotencyConfig.HashFunction); + if (hashAlgorithm == null) + { + throw new ArgumentException("Invalid HashAlgorithm"); + } + var stringToHash = node is JValue ? node.ToString() : node.ToString(Formatting.None); + string hash = GetHash(hashAlgorithm, stringToHash); + + return hash; + } + + private static string GetHash(HashAlgorithm hashAlgorithm, string input) + { + // Convert the input string to a byte array and compute the hash. + byte[] data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input)); + + // Create a new Stringbuilder to collect the bytes + // and create a string. + var sBuilder = new StringBuilder(); + + // Loop through each byte of the hashed data + // and format each one as a hexadecimal string. + for (int i = 0; i < data.Length; i++) + { + sBuilder.Append(data[i].ToString("x2")); + } + + // Return the hexadecimal string. + return sBuilder.ToString(); + } + + public abstract Task GetRecord(string idempotencyKey); + + public abstract Task PutRecord(DataRecord record, DateTimeOffset now); + + public abstract Task UpdateRecord(DataRecord record); + + public abstract Task DeleteRecord(string idempotencyKey); +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs new file mode 100644 index 00000000..08c0afec --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs @@ -0,0 +1,98 @@ +/* + * 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. + */ + +namespace AWS.Lambda.Powertools.Idempotency.Persistence; + +public class DataRecord +{ + private readonly string _status; + + public DataRecord(string idempotencyKey, + DataRecordStatus status, + long expiryTimestamp, + string? responseData, + string? payloadHash) + { + IdempotencyKey = idempotencyKey; + _status = status.ToString(); + ExpiryTimestamp = expiryTimestamp; + ResponseData = responseData; + PayloadHash = payloadHash; + } + + public string IdempotencyKey { get; } + public long ExpiryTimestamp { get; } + public string? ResponseData { get; } + public string? PayloadHash { get; } + + + /// + /// Check if data record is expired (based on expiration configured in the IdempotencyConfig + /// + /// + /// Whether the record is currently expired or not + public bool IsExpired(DateTimeOffset now) + { + return ExpiryTimestamp != 0 && now.ToUnixTimeSeconds() > ExpiryTimestamp; + } + + public DataRecordStatus Status + { + get + { + var now = DateTimeOffset.UtcNow; + if (IsExpired(now)) + { + return DataRecordStatus.EXPIRED; + } + + return Enum.Parse(_status); + } + } + + protected bool Equals(DataRecord other) + { + return _status == other._status + && IdempotencyKey == other.IdempotencyKey + && ExpiryTimestamp == other.ExpiryTimestamp + && ResponseData == other.ResponseData + && PayloadHash == other.PayloadHash; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DataRecord) obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(IdempotencyKey, _status, ExpiryTimestamp, ResponseData, PayloadHash); + } + + /// + /// Status of the record: + /// -- INPROGRESS: record initialized when function starts + /// -- COMPLETED: record updated with the result of the function when it ends + /// -- EXPIRED: record expired, idempotency will not happen + /// + public enum DataRecordStatus { + INPROGRESS, + COMPLETED, + EXPIRED + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs new file mode 100644 index 00000000..a9d43ff7 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -0,0 +1,371 @@ +/* + * 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 Amazon; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using AWS.Lambda.Powertools.Idempotency.Exceptions; + +namespace AWS.Lambda.Powertools.Idempotency.Persistence; + +public class DynamoDBPersistenceStore : BasePersistenceStore +{ + private readonly string _tableName; + private readonly string _keyAttr; + private readonly string _staticPkValue; + private readonly string? _sortKeyAttr; + private readonly string _expiryAttr; + private readonly string _statusAttr; + private readonly string _dataAttr; + private readonly string _validationAttr; + private readonly AmazonDynamoDBClient _dynamoDbClient; + + private DynamoDBPersistenceStore(string tableName, + string keyAttr, + string staticPkValue, + string? sortKeyAttr, + string expiryAttr, + string statusAttr, + string dataAttr, + string validationAttr, + AmazonDynamoDBClient client) + { + _tableName = tableName; + _keyAttr = keyAttr; + _staticPkValue = staticPkValue; + _sortKeyAttr = sortKeyAttr; + _expiryAttr = expiryAttr; + _statusAttr = statusAttr; + _dataAttr = dataAttr; + _validationAttr = validationAttr; + + if (client != null) + { + _dynamoDbClient = client; + } + else + { + string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV); + if (idempotencyDisabledEnv == null || idempotencyDisabledEnv.Equals("false")) + { + AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig + { + RegionEndpoint = RegionEndpoint.GetBySystemName(Environment.GetEnvironmentVariable(Constants.AWS_REGION_ENV)) + }; + _dynamoDbClient = new AmazonDynamoDBClient(clientConfig); + } else { + // we do not want to create a DynamoDbClient if idempotency is disabled + // null is ok as idempotency won't be called + _dynamoDbClient = null; + } + } + } + + + public override async Task GetRecord(string idempotencyKey) + { + var getItemRequest = new GetItemRequest() + { + TableName = _tableName, + ConsistentRead = true, + Key = GetKey(idempotencyKey) + }; + GetItemResponse response = await _dynamoDbClient.GetItemAsync(getItemRequest); + + if (!response.IsItemSet) + { + throw new IdempotencyItemNotFoundException(idempotencyKey); + } + + return ItemToRecord(response.Item); + } + + + public override async Task PutRecord(DataRecord record, DateTimeOffset now) + { + Dictionary item = new(GetKey(record.IdempotencyKey)); + item.Add(this._expiryAttr, new AttributeValue() + { + N = record.ExpiryTimestamp.ToString() + }); + item.Add(this._statusAttr, new AttributeValue(record.Status.ToString())); + + if (PayloadValidationEnabled) + { + item.Add(this._validationAttr, new AttributeValue(record.PayloadHash)); + } + + try + { + Log.WriteDebug("Putting record for idempotency key: {0}", record.IdempotencyKey); + + var expressionAttributeNames = new Dictionary + { + {"#id", this._keyAttr}, + {"#expiry", this._expiryAttr} + }; + + PutItemRequest request = new PutItemRequest() + { + TableName = _tableName, + Item = item, + ConditionExpression = "attribute_not_exists(#id) OR #expiry < :now", + ExpressionAttributeNames = expressionAttributeNames, + ExpressionAttributeValues = new Dictionary + { + {":now", new AttributeValue() {N = now.ToUnixTimeSeconds().ToString()}} + } + }; + await _dynamoDbClient.PutItemAsync(request); + } + catch (ConditionalCheckFailedException e) + { + Log.WriteDebug("Failed to put record for already existing idempotency key: {0}", record.IdempotencyKey); + throw new IdempotencyItemAlreadyExistsException( + "Failed to put record for already existing idempotency key: " + record.IdempotencyKey, e); + } + } + + + public override async Task UpdateRecord(DataRecord record) + { + Log.WriteDebug("Updating record for idempotency key: {0}", record.IdempotencyKey); + string updateExpression = "SET #response_data = :response_data, #expiry = :expiry, #status = :status"; + + var expressionAttributeNames = new Dictionary + { + {"#response_data", this._dataAttr}, + {"#expiry", this._expiryAttr}, + {"#status", this._statusAttr} + }; + + var expressionAttributeValues = new Dictionary + { + {":response_data", new AttributeValue(record.ResponseData)}, + {":expiry", new AttributeValue(){N=record.ExpiryTimestamp.ToString()}}, + {":status", new AttributeValue(record.Status.ToString())} + }; + + if (PayloadValidationEnabled) + { + updateExpression += ", #validation_key = :validation_key"; + expressionAttributeNames.Add("#validation_key", this._validationAttr); + expressionAttributeValues.Add(":validation_key", new AttributeValue(record.PayloadHash)); + } + + var request = new UpdateItemRequest + { + TableName = _tableName, + Key = GetKey(record.IdempotencyKey), + UpdateExpression = updateExpression, + ExpressionAttributeNames = expressionAttributeNames, + ExpressionAttributeValues = expressionAttributeValues + }; + await _dynamoDbClient.UpdateItemAsync(request); + } + + public override async Task DeleteRecord(string idempotencyKey) + { + Log.WriteDebug("Deleting record for idempotency key: {0}", idempotencyKey); + var request = new DeleteItemRequest + { + TableName = _tableName, + Key = GetKey(idempotencyKey) + }; + await _dynamoDbClient.DeleteItemAsync(request); + } + + /// + /// Translate raw item records from DynamoDB to DataRecord + /// + /// item Item from dynamodb response + /// DataRecord instance + private DataRecord ItemToRecord(Dictionary item) + { + // data and validation payload may be null + var hasDataAttribute = item.TryGetValue(_dataAttr, out AttributeValue? data); + var hasValidationAttribute = item.TryGetValue(_validationAttr, out AttributeValue? validation); + + return new DataRecord(item[_sortKeyAttr != null ? _sortKeyAttr : _keyAttr].S, + Enum.Parse(item[_statusAttr].S), + long.Parse(item[_expiryAttr].N), + hasDataAttribute ? data?.S : null, + hasValidationAttribute ? validation?.S : null); + } + + /// + /// Get the key to use for requests (depending on if we have a sort key or not) + /// + /// idempotencyKey + /// + private Dictionary GetKey(string idempotencyKey) + { + Dictionary key = new(); + if (_sortKeyAttr != null) + { + key[_keyAttr] = new AttributeValue(this._staticPkValue); + key[_sortKeyAttr] = new AttributeValue(idempotencyKey); + } + else + { + key[_keyAttr] = new AttributeValue(idempotencyKey); + } + + return key; + } + + public static DynamoDBPersistenceStoreBuilder Builder() => new DynamoDBPersistenceStoreBuilder(); + + public class DynamoDBPersistenceStoreBuilder + { + private static readonly string? FuncEnv = Environment.GetEnvironmentVariable(Constants.LAMBDA_FUNCTION_NAME_ENV); + + private string _tableName = null!; + private string _keyAttr = "id"; + private string _staticPkValue = string.Format("idempotency#%s", FuncEnv ?? ""); + private string? _sortKeyAttr = null; + private string _expiryAttr = "expiration"; + private string _statusAttr = "status"; + private string _dataAttr = "data"; + private string _validationAttr = "validation"; + private AmazonDynamoDBClient _dynamoDbClient; + + /// + /// Initialize and return a new instance of {@link DynamoDBPersistenceStore}. + /// Example: + /// DynamoDBPersistenceStore.builder().withTableName("idempotency_store").build(); + /// + /// + /// + public DynamoDBPersistenceStore Build() + { + if (string.IsNullOrWhiteSpace(_tableName)) + { + throw new ArgumentNullException("Table name is not specified"); + } + return new DynamoDBPersistenceStore(_tableName, + _keyAttr, + _staticPkValue, + _sortKeyAttr, + _expiryAttr, + _statusAttr, + _dataAttr, + _validationAttr, + _dynamoDbClient); + } + + /// + /// Name of the table to use for storing execution records (mandatory) + /// + /// tableName Name of the DynamoDB table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithTableName(string tableName) + { + _tableName = tableName; + return this; + } + + /// + /// DynamoDB attribute name for partition key (optional), by default "id" + /// + /// keyAttr name of the key attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithKeyAttr(string keyAttr) + { + _keyAttr = keyAttr; + return this; + } + + /// + /// DynamoDB attribute value for partition key (optional), by default "idempotency#[function-name]". + /// This will be used if the {@link #sortKeyAttr} is set. + /// + /// staticPkValue name of the partition key attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithStaticPkValue(string staticPkValue) + { + _staticPkValue = staticPkValue; + return this; + } + + /// + /// DynamoDB attribute name for the sort key (optional) + /// + /// sortKeyAttr name of the sort key attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithSortKeyAttr(string sortKeyAttr) + { + _sortKeyAttr = sortKeyAttr; + return this; + } + + /// + /// DynamoDB attribute name for expiry timestamp (optional), by default "expiration" + /// + /// expiryAttr name of the expiry attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithExpiryAttr(string expiryAttr) + { + _expiryAttr = expiryAttr; + return this; + } + + /// + /// DynamoDB attribute name for status (optional), by default "status" + /// + /// statusAttr name of the status attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithStatusAttr(string statusAttr) + { + _statusAttr = statusAttr; + return this; + } + + /// + /// DynamoDB attribute name for response data (optional), by default "data" + /// + /// dataAttr name of the data attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithDataAttr(string dataAttr) + { + _dataAttr = dataAttr; + return this; + } + + /// + /// DynamoDB attribute name for validation (optional), by default "validation" + /// + /// validationAttr name of the validation attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithValidationAttr(string validationAttr) + { + _validationAttr = validationAttr; + return this; + } + + /// + /// Custom DynamoDbClient used to query DynamoDB (optional). + /// The default one uses UrlConnectionHttpClient as a http client and + /// + /// dynamoDbClient the DynamoDbClient instance to use + /// the builder instance (to chain operations) + // ReSharper disable once InconsistentNaming + public DynamoDBPersistenceStoreBuilder WithDynamoDBClient(AmazonDynamoDBClient dynamoDbClient) + { + _dynamoDbClient = dynamoDbClient; + return this; + } + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs new file mode 100644 index 00000000..978a62d1 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs @@ -0,0 +1,55 @@ +/* + * 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 AWS.Lambda.Powertools.Idempotency.Exceptions; + +namespace AWS.Lambda.Powertools.Idempotency.Persistence; + +/// +/// Persistence layer that will store the idempotency result. +/// In order to provide another implementation, extends {@link BasePersistenceStore}. +/// +public interface IPersistenceStore +{ + + /// + /// Retrieve item from persistence store using idempotency key and return it as a DataRecord instance. + /// + /// idempotencyKey the key of the record + /// DataRecord representation of existing record found in persistence store + /// Exception thrown if no record exists in persistence store with the idempotency key + Task GetRecord(string idempotencyKey); + + /// + /// Add a DataRecord to persistence store if it does not already exist with that key + /// + /// record DataRecord instance + /// + /// + /// if a non-expired entry already exists. + Task PutRecord(DataRecord record, DateTimeOffset now); + + /// + /// Update item in persistence store + /// + /// DataRecord instance + Task UpdateRecord(DataRecord record); + + /// + /// Remove item from persistence store + /// + /// idempotencyKey the key of the record + Task DeleteRecord(string idempotencyKey); +} diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs new file mode 100644 index 00000000..484cc65e --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs @@ -0,0 +1,36 @@ +/* + * 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 DevLab.JmesPath.Functions; +using Newtonsoft.Json.Linq; + +namespace AWS.Lambda.Powertools.Idempotency.Serialization; + +public class JsonFunction : JmesPathFunction +{ + public JsonFunction() + : base("powertools_json", 1) + { + } + + public override JToken Execute(params JmesPathFunctionArgument[] args) + { + System.Diagnostics.Debug.Assert(args.Length == 1); + System.Diagnostics.Debug.Assert(args[0].IsToken); + var argument = args[0]; + var token = argument.Token; + return JToken.Parse(token.ToString()); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj new file mode 100644 index 00000000..205ba897 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj @@ -0,0 +1,44 @@ + + + + net6.0 + + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + PreserveNewest + + + + PreserveNewest + + + + diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs new file mode 100644 index 00000000..272a88c6 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs @@ -0,0 +1,36 @@ +/* + * 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.Threading.Tasks; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.Idempotency.Tests.Model; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; + +public class IdempotencyEnabledFunction +{ + public bool HandlerExecuted = false; + + [Idempotent] + public Task Handle(Product input, ILambdaContext context) + { + HandlerExecuted = true; + Basket basket = new Basket(); + basket.Add(input); + var result = Task.FromResult(basket); + + return result; + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs new file mode 100644 index 00000000..8d32f87b --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs @@ -0,0 +1,101 @@ +/* + * 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.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.Idempotency.Persistence; +using Newtonsoft.Json.Linq; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; + +public class IdempotencyFunction +{ + public bool HandlerExecuted = false; + + public IdempotencyFunction(AmazonDynamoDBClient client) + { + AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + .WithConfig( + IdempotencyConfig.Builder() + .WithEventKeyJmesPath("powertools_json(Body).address") + .Build()) + .WithPersistenceStore( + DynamoDBPersistenceStore.Builder() + .WithTableName("idempotency_table") + .WithDynamoDBClient(client) + .Build() + ).Configure(); + + } + + [Idempotent] + public async Task Handle(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + HandlerExecuted = true; + var result= await InternalFunctionHandler(apigProxyEvent,context); + + return result; + } + private async Task InternalFunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + { + Dictionary headers = new() + { + {"Content-Type", "application/json"}, + {"Access-Control-Allow-Origin", "*"}, + {"Access-Control-Allow-Methods", "GET, OPTIONS"}, + {"Access-Control-Allow-Headers", "*"} + }; + + try + { + string address = JToken.Parse(apigProxyEvent.Body)["address"].Value(); + string pageContents = await getPageContents(address); + string output = $"{{ \"message\": \"hello world\", \"location\": \"{pageContents}\" }}"; + + return new APIGatewayProxyResponse + { + Body = output, + StatusCode = 200, + Headers = headers + }; + + } + catch (IOException e) + { + return new APIGatewayProxyResponse + { + Body = "{}", + StatusCode = 500, + Headers = headers + }; + } + } + + // we could actually also put the @Idempotent annotation here + private async Task getPageContents(string address) + { + HttpClient client = new HttpClient(); + using HttpResponseMessage response = await client.GetAsync(address); + using HttpContent content = response.Content; + string pageContent = await content.ReadAsStringAsync(); + + return pageContent; + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs new file mode 100644 index 00000000..645f3fc2 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs @@ -0,0 +1,28 @@ +/* + * 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.Threading.Tasks; +using Amazon.Lambda.Core; +using AWS.Lambda.Powertools.Idempotency.Tests.Model; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; + +public class IdempotencyWithErrorFunction +{ + [Idempotent] + public Task Handle(Product input, ILambdaContext context) + => throw new IndexOutOfRangeException("Fake exception"); +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs new file mode 100644 index 00000000..cb2b2913 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -0,0 +1,57 @@ +/* + * 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.IO; +using System.Threading.Tasks; +using Amazon.DynamoDBv2.Model; +using Amazon.Lambda.APIGatewayEvents; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Idempotency.Tests.Handlers; +using FluentAssertions; +using Newtonsoft.Json; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests; + +public class IdempotencyTest : IntegrationTestBase +{ + [Fact] + public async Task EndToEndTest() + { + IdempotencyFunction function = new IdempotencyFunction(client); + + //var persistenceStore = new InMemoryPersistenceStore(); + TestLambdaContext context = new TestLambdaContext(); + var request = JsonConvert.DeserializeObject(File.ReadAllText("./resources/apigw_event2.json")); + + + APIGatewayProxyResponse response = await function.Handle(request, context); + function.HandlerExecuted.Should().BeTrue(); + + function.HandlerExecuted = false; + + var response2 = await function.Handle(request, context); + function.HandlerExecuted.Should().BeFalse(); + + JsonConvert.SerializeObject(response).Should().Be(JsonConvert.SerializeObject(response)); + response2.Body.Should().Contain("hello world"); + + var scanResponse = await client.ScanAsync(new ScanRequest + { + TableName = TABLE_NAME + }); + scanResponse.Count.Should().Be(1); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs new file mode 100644 index 00000000..918d800a --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs @@ -0,0 +1,79 @@ +/* + * 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.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests; + +public class IntegrationTestBase: IAsyncLifetime +{ + protected const string TABLE_NAME = "idempotency_table"; + protected AmazonDynamoDBClient client; + private TestcontainersContainer _testContainer; + + public virtual async Task InitializeAsync() + { + _testContainer = new TestcontainersBuilder() + .WithImage("amazon/dynamodb-local:latest") + .WithPortBinding(8000, assignRandomHostPort:true) + .WithDockerEndpoint(Environment.GetEnvironmentVariable("DOCKER_HOST") ?? "unix:///var/run/docker.sock") + .Build(); + await _testContainer.StartAsync(); + var credentials = new BasicAWSCredentials("FAKE", "FAKE"); + var amazonDynamoDbConfig = new AmazonDynamoDBConfig() + { + ServiceURL = $"http://localhost:{_testContainer.GetMappedPublicPort(8000)}", + AuthenticationRegion = "us-east-1" + }; + client = new AmazonDynamoDBClient(credentials, amazonDynamoDbConfig); + + var createTableRequest = new CreateTableRequest + { + TableName = TABLE_NAME, + KeySchema = new List() + { + new("id", KeyType.HASH) + }, + AttributeDefinitions = new List() + { + new() + { + AttributeName = "id", + AttributeType = ScalarAttributeType.S + } + }, + BillingMode = BillingMode.PAY_PER_REQUEST + }; + await client.CreateTableAsync(createTableRequest); + var response = await client.DescribeTableAsync(TABLE_NAME); + if (response == null) + { + throw new NullReferenceException("Table was not created within the expected time"); + } + } + + public virtual Task DisposeAsync() + { + return _testContainer.DisposeAsync().AsTask(); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs new file mode 100644 index 00000000..f437a41e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -0,0 +1,179 @@ +/* + * 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.Threading.Tasks; +using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Persistence; +using AWS.Lambda.Powertools.Idempotency.Tests.Handlers; +using AWS.Lambda.Powertools.Idempotency.Tests.Model; +using FluentAssertions; +using Moq; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; + +public class IdempotentAspectTests +{ + [Fact] + public async Task FirstCall_ShouldPutInStore() + { + var store = new Mock(); + AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + .WithPersistenceStore(store.Object) + .WithConfig(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("Id") + .Build() + ).Configure(); + + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + + Product p = new Product(42, "fake product", 12); + Basket basket = await function.Handle(p, new TestLambdaContext()); + basket.Products.Count.Should().Be(1); + function.HandlerExecuted.Should().BeTrue(); + + store + .Verify(x=>x.SaveInProgress(It.Is(t=> t.ToString() == JToken.FromObject(p).ToString()), It.IsAny())); + + store + .Verify(x=>x.SaveSuccess(It.IsAny(), It.Is(x => x.Equals(basket)), It.IsAny())); + } + + [Fact] + public async Task SecondCall_NotExpired_ShouldGetFromStore() + { + var store = new Mock(); + store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) + .Throws(); + + // GIVEN + AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + .WithPersistenceStore(store.Object) + .WithConfig(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("Id") + .Build() + ).Configure(); + + Product p = new Product(42, "fake product", 12); + Basket b = new Basket(p); + DataRecord record = new DataRecord( + "42", + DataRecord.DataRecordStatus.COMPLETED, + DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), + JToken.FromObject(b).ToString(), + null); + store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) + .ReturnsAsync(record); + + // WHEN + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + Basket basket = await function.Handle(p, new TestLambdaContext()); + + // THEN + basket.Should().Be(b); + function.HandlerExecuted.Should().BeFalse(); + } + + [Fact] + public async Task SecondCall_InProgress_ShouldThrowIdempotencyAlreadyInProgressException() + { + var store = new Mock(); + // GIVEN + AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + .WithPersistenceStore(store.Object) + .WithConfig(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("Id") + .Build() + ).Configure(); + store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) + .Throws(); + + Product p = new Product(42, "fake product", 12); + Basket b = new Basket(p); + DataRecord record = new DataRecord( + "42", + DataRecord.DataRecordStatus.INPROGRESS, + DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), + JToken.FromObject(b).ToString(), + null); + store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) + .ReturnsAsync(record); + + // THEN + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + Func act = async () => await function.Handle(p, new TestLambdaContext()); + + await act.Should().ThrowAsync(); + + } + + [Fact] + public async Task FunctionThrowException_ShouldDeleteRecord_AndThrowFunctionException() + { + var store = new Mock(); + // GIVEN + AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + .WithPersistenceStore(store.Object) + .WithConfig(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("Id") + .Build() + ).Configure(); + + // WHEN / THEN + IdempotencyWithErrorFunction function = new IdempotencyWithErrorFunction(); + + Product p = new Product(42, "fake product", 12); + Func act = async () => await function.Handle(p, new TestLambdaContext()); + + await act.Should().ThrowAsync(); + + store.Verify( + x => x.DeleteRecord(It.IsAny(), It.IsAny())); + } + + [Fact] + public async Task TestIdempotencyDisabled_ShouldJustRunTheFunction() + { + try + { + var store = new Mock(); + Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); + // GIVEN + AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + .WithPersistenceStore(store.Object) + .WithConfig(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("Id") + .Build() + ).Configure(); + + // WHEN + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + Product p = new Product(42, "fake product", 12); + Basket basket = await function.Handle(p, new TestLambdaContext()); + + // THEN + store.Invocations.Count.Should().Be(0); + basket.Products.Count.Should().Be(1); + function.HandlerExecuted.Should().BeTrue(); + } + finally + { + Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "false"); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTest.cs new file mode 100644 index 00000000..7236610f --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTest.cs @@ -0,0 +1,167 @@ +/* + * 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.Collections.Generic; +using System.Threading.Tasks; +using AWS.Lambda.Powertools.Idempotency.Internal; +using Xunit; + +//Source: https://github.dev/microsoft/botbuilder-dotnet/blob/main/tests/AdaptiveExpressions.Tests/LRUCacheTest.cs +namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; + +public class LRUCacheTest +{ + [Fact] + public void TestBasic() + { + var cache = new LRUCache(2); + + Assert.False(cache.TryGet(1, out var result)); + + cache.Set(1, "num1"); + + Assert.True(cache.TryGet(1, out result)); + Assert.Equal("num1", result); + } + + [Fact] + public void TestDiacardPolicy() + { + var cache = new LRUCache(2); + cache.Set(1, "num1"); + cache.Set(2, "num2"); + cache.Set(3, "num3"); + + // should be {2,'num2'} and {3, 'num3'} + Assert.False(cache.TryGet(1, out var result)); + + Assert.True(cache.TryGet(2, out result)); + Assert.Equal("num2", result); + + Assert.True(cache.TryGet(3, out result)); + Assert.Equal("num3", result); + } + + [Fact] + /* + * The average time of this test is about 2ms. + */ + public void TestDPMemorySmall() + { + var cache = new LRUCache(2); + cache.Set(0, 1); + cache.Set(1, 1); + const int fib9999 = 1242044891; + const int fib100000 = 2132534333; + const int maxIdx = 10000; + for (var i = 2; i <= maxIdx; i++) + { + cache.TryGet(i - 2, out var prev2); + cache.TryGet(i - 1, out var prev1); + cache.Set(i, prev1 + prev2); + } + + Assert.False(cache.TryGet(9998, out var result)); + + Assert.True(cache.TryGet(maxIdx - 1, out result)); + Assert.Equal(result, fib9999); + + Assert.True(cache.TryGet(maxIdx, out result)); + Assert.Equal(result, fib100000); + } + + + /* + * The average time of this test is about 3ms. + */ + [Fact] + public void TestDPMemoryLarge() + { + var cache = new LRUCache(500); + cache.Set(0, 1); + cache.Set(1, 1); + const int fib9999 = 1242044891; + const int fib100000 = 2132534333; + const int maxIdx = 10000; + for (var i = 2; i <= 10000; i++) + { + cache.TryGet(i - 2, out var prev2); + cache.TryGet(i - 1, out var prev1); + cache.Set(i, prev1 + prev2); + } + + Assert.False(cache.TryGet(1, out var result)); + + Assert.True(cache.TryGet(maxIdx - 1, out result)); + Assert.Equal(result, fib9999); + + Assert.True(cache.TryGet(maxIdx, out result)); + Assert.Equal(result, fib100000); + } + + [Fact] + /* + * The average time of this test is about 13ms(without the loop of Assert statements). + */ + public async Task TestMultiThreadingAsync() + { + var cache = new LRUCache(10); + var tasks = new List(); + const int numOfThreads = 10; + const int numOfOps = 1000; + for (var i = 0; i < numOfThreads; i++) + { + tasks.Add(Task.Run(() => StoreElement(cache, numOfOps, i))); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + + for (var i = numOfOps - numOfThreads; i < numOfOps; i++) + { + Assert.True(cache.TryGet(i, out var result)); + } + } + + + [Fact] + public void TestDelete() + { + var cache = new LRUCache(3); + cache.Set(1, "num1"); + cache.Set(2, "num2"); + cache.Set(3, "num3"); + + cache.Delete(1); + + // should be {2,'num2'} and {3, 'num3'} + Assert.False(cache.TryGet(1, out var result)); + + Assert.True(cache.TryGet(2, out result)); + Assert.Equal("num2", result); + + Assert.True(cache.TryGet(3, out result)); + Assert.Equal("num3", result); + } + + private void StoreElement(LRUCache cache, int numOfOps, int idx) + { + for (var i = 0; i < numOfOps; i++) + { + var key = i; + var value = i; + cache.Set(key, value); + } + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs new file mode 100644 index 00000000..a34c090a --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs @@ -0,0 +1,56 @@ +/* + * 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.Collections.Generic; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Model; + +public class Basket +{ + public List Products { get; set; } = new List(); + public Basket() + { + + } + + public Basket(params Product[] products) + { + Products.AddRange(products); + } + + public void Add(Product product) + { + Products.Add(product); + } + + protected bool Equals(Basket other) + { + var products = Products; + return products.Equals(products); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Basket) obj); + } + + public override int GetHashCode() + { + return Products.GetHashCode(); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs new file mode 100644 index 00000000..5b156d87 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs @@ -0,0 +1,55 @@ +/* + * 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; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Model; + +public class Product +{ + public long Id { get; set; } + public string Name { get; set; } + public double Price { get; set; } + + public Product() + { + + } + + public Product(long id, string name, double price) + { + Id = id; + Name = name; + Price = price; + } + + protected bool Equals(Product other) + { + return Id == other.Id && Name == other.Name && Price.Equals(other.Price); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((Product) obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(Id, Name, Price); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs new file mode 100644 index 00000000..fefec3e7 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -0,0 +1,427 @@ +/* + * 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.IO; +using System.Threading.Tasks; +using Amazon.Lambda.APIGatewayEvents; +using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Internal; +using AWS.Lambda.Powertools.Idempotency.Persistence; +using AWS.Lambda.Powertools.Idempotency.Tests.Model; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; + +public class BasePersistenceStoreTests +{ + class InMemoryPersistenceStore : BasePersistenceStore + { + private string _validationHash = null; + public DataRecord? DataRecord = null; + public int Status = -1; + public override Task GetRecord(string idempotencyKey) + { + Status = 0; + var dataRecord = new DataRecord( + idempotencyKey, + DataRecord.DataRecordStatus.INPROGRESS, + DateTimeOffset.UtcNow.AddSeconds(3600).ToUnixTimeSeconds(), + "Response", + _validationHash); + return Task.FromResult(dataRecord); + } + + public override Task PutRecord(DataRecord record, DateTimeOffset now) + { + DataRecord = record; + Status = 1; + return Task.CompletedTask; + } + + public override Task UpdateRecord(DataRecord record) + { + DataRecord = record; + Status = 2; + return Task.CompletedTask; + } + + public override Task DeleteRecord(string idempotencyKey) + { + DataRecord = null; + Status = 3; + return Task.CompletedTask; + } + } + + [Fact] + public async Task SaveInProgress_DefaultConfig() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + + DateTimeOffset now = DateTimeOffset.UtcNow; + + await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + + var dr = persistenceStore.DataRecord; + dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); + dr.ResponseData.Should().BeNull(); + dr.IdempotencyKey.Should().Be("testFunction#36e3de9a3270f82fb957c645178dfab9"); + dr.PayloadHash.Should().BeEmpty(); + persistenceStore.Status.Should().Be(1); + } + + + + [Fact] + public async Task SaveInProgress_jmespath() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("powertools_json(Body).id") + .Build(), "myfunc"); + + DateTimeOffset now = DateTimeOffset.UtcNow; + await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + + var dr = persistenceStore.DataRecord; + dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); + dr.ResponseData.Should().BeNull(); + dr.IdempotencyKey.Should().Be("testFunction.myfunc#2fef178cc82be5ce3da6c5e0466a6182"); + dr.PayloadHash.Should().BeEmpty(); + persistenceStore.Status.Should().Be(1); + } + + + [Fact] + public async Task SaveInProgress_JMESPath_NotFound_ShouldThrowException() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("unavailable") + .WithThrowOnNoIdempotencyKey(true) // should throw + .Build(), ""); + DateTimeOffset now = DateTimeOffset.UtcNow; + + Func act = async () => await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + await act.Should() + .ThrowAsync() + .WithMessage("No data found to create a hashed idempotency key"); + + persistenceStore.Status.Should().Be(-1); + } + + [Fact] + public async Task SaveInProgress_JMESpath_NotFound_ShouldNotThrowException() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("unavailable") + .Build(), ""); + + DateTimeOffset now = DateTimeOffset.UtcNow; + await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + + DataRecord dr = persistenceStore.DataRecord; + dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + persistenceStore.Status.Should().Be(1); + } + + [Fact] + public async Task SaveInProgress_WithLocalCache_NotExpired_ShouldThrowException() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + LRUCache cache = new ((int) 2); + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithUseLocalCache(true) + .WithEventKeyJmesPath("powertools_json(Body).id") + .Build(), null, cache); + + DateTimeOffset now = DateTimeOffset.UtcNow; + cache.Set("testFunction#2fef178cc82be5ce3da6c5e0466a6182", + new DataRecord( + "testFunction#2fef178cc82be5ce3da6c5e0466a6182", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(3600).ToUnixTimeSeconds(), + null, null) + ); + + Func act = () => persistenceStore.SaveInProgress(JToken.FromObject(request), now); + + await act.Should() + .ThrowAsync(); + + persistenceStore.Status.Should().Be(-1); + } + + [Fact] + public async Task SaveInProgress_WithLocalCache_Expired_ShouldRemoveFromCache() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + LRUCache cache = new ((int) 2); + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("powertools_json(Body).id") + .WithUseLocalCache(true) + .WithExpiration(TimeSpan.FromSeconds(2)) + .Build(), null, cache); + + DateTimeOffset now = DateTimeOffset.UtcNow; + cache.Set("testFunction#2fef178cc82be5ce3da6c5e0466a6182", + new DataRecord( + "testFunction#2fef178cc82be5ce3da6c5e0466a6182", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(-3).ToUnixTimeSeconds(), + null, null) + ); + + await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + + DataRecord dr = persistenceStore.DataRecord; + dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + cache.Count.Should().Be(0); + persistenceStore.Status.Should().Be(1); + } + + ////// Save Success + + [Fact] + public async Task SaveSuccess_ShouldUpdateRecord() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + LRUCache cache = new ((int) 2); + persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null, cache); + + Product product = new Product(34543, "product", 42); + + DateTimeOffset now = DateTimeOffset.UtcNow; + await persistenceStore.SaveSuccess(JToken.FromObject(request), product, now); + + DataRecord dr = persistenceStore.DataRecord; + dr.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); + dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); + dr.ResponseData.Should().Be(JsonConvert.SerializeObject(product)); + dr.IdempotencyKey.Should().Be("testFunction#36e3de9a3270f82fb957c645178dfab9"); + dr.PayloadHash.Should().BeEmpty(); + persistenceStore.Status.Should().Be(2); + cache.Count.Should().Be(0); + } + + [Fact] + public async Task SaveSuccess_WithCacheEnabled_ShouldSaveInCache() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + LRUCache cache = new ((int) 2); + + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithUseLocalCache(true).Build(), null, cache); + + Product product = new Product(34543, "product", 42); + DateTimeOffset now = DateTimeOffset.UtcNow; + + await persistenceStore.SaveSuccess(JToken.FromObject(request), product, now); + + persistenceStore.Status.Should().Be(2); + cache.Count.Should().Be(1); + + var foundDataRecord = cache.TryGet("testFunction#36e3de9a3270f82fb957c645178dfab9", out DataRecord record); + foundDataRecord.Should().BeTrue(); + record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); + record.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); + record.ResponseData.Should().Be(JsonConvert.SerializeObject(product)); + record.IdempotencyKey.Should().Be("testFunction#36e3de9a3270f82fb957c645178dfab9"); + record.PayloadHash.Should().BeEmpty(); + } + + /// Get Record + + [Fact] + public async Task GetRecord_ShouldReturnRecordFromPersistence() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + LRUCache cache = new((int) 2); + persistenceStore.Configure(IdempotencyConfig.Builder().Build(), "myfunc", cache); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); + record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + record.ResponseData.Should().Be("Response"); + persistenceStore.Status.Should().Be(0); + } + + [Fact] + + public async Task GetRecord_CacheEnabledNotExpired_ShouldReturnRecordFromCache() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + LRUCache cache = new((int) 2); + + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithUseLocalCache(true).Build(), "myfunc", cache); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DataRecord dr = new DataRecord( + "testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", + DataRecord.DataRecordStatus.COMPLETED, + now.AddSeconds(3600).ToUnixTimeSeconds(), + "result of the function", + null); + cache.Set("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", dr); + + DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); + record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); + record.ResponseData.Should().Be("result of the function"); + persistenceStore.Status.Should().Be(-1); + } + + [Fact] + public async Task GetRecord_CacheEnabledExpired_ShouldReturnRecordFromPersistence() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + LRUCache cache = new((int) 2); + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithUseLocalCache(true).Build(), "myfunc", cache); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DataRecord dr = new DataRecord( + "testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", + DataRecord.DataRecordStatus.COMPLETED, + now.AddSeconds(-3).ToUnixTimeSeconds(), + "result of the function", + null); + cache.Set("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", dr); + + DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); + record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + record.ResponseData.Should().Be("Response"); + persistenceStore.Status.Should().Be(0); + cache.Count.Should().Be(0); + } + + [Fact] + public async Task GetRecord_InvalidPayload_ShouldThrowValidationException() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("powertools_json(Body).id") + .WithPayloadValidationJmesPath("powertools_json(Body).message") + .Build(), + "myfunc"); + + var validationHash = "different hash"; // "Lambda rocks" ==> 70c24d88041893f7fbab4105b76fd9e1 + DateTimeOffset now = DateTimeOffset.UtcNow; + Func act = () => persistenceStore.GetRecord(JToken.FromObject(request), now); + await act.Should().ThrowAsync(); + } + + // Delete Record + [Fact] + public async Task DeleteRecord_ShouldDeleteRecordFromPersistence() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + + await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); + persistenceStore.Status.Should().Be(3); + } + + [Fact] + public async Task DeleteRecord_CacheEnabled_ShouldDeleteRecordFromCache() + { + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + LRUCache cache = new ((int) 2); + persistenceStore.Configure(IdempotencyConfig.Builder() + .WithUseLocalCache(true).Build(), null, cache); + + cache.Set("testFunction#36e3de9a3270f82fb957c645178dfab9", + new DataRecord("testFunction#36e3de9a3270f82fb957c645178dfab9", + DataRecord.DataRecordStatus.COMPLETED, + 123, + null, null)); + + await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); + persistenceStore.Status.Should().Be(3); + cache.Count.Should().Be(0); + } + + [Fact] + public void GenerateHashString_ShouldGenerateMd5ofString() + { + var persistenceStore = new InMemoryPersistenceStore(); + persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + string expectedHash = "70c24d88041893f7fbab4105b76fd9e1"; // MD5(Lambda rocks) + string generatedHash = persistenceStore.GenerateHash(new JValue("Lambda rocks")); + generatedHash.Should().Be(expectedHash); + } + + [Fact] + public void GenerateHashObject_ShouldGenerateMd5ofJsonObject() + { + var persistenceStore = new InMemoryPersistenceStore(); + persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + Product product = new Product(42, "Product", 12); + string expectedHash = "87dd2e12074c65c9bac728795a6ebb45"; // MD5({"Id":42,"Name":"Product","Price":12.0}) + string generatedHash = persistenceStore.GenerateHash(JToken.FromObject(product)); + generatedHash.Should().Be(expectedHash); + } + + [Fact] + public void GenerateHashDouble_ShouldGenerateMd5ofDouble() + { + var persistenceStore = new InMemoryPersistenceStore(); + persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + string expectedHash = "bb84c94278119c8838649706df4db42b"; // MD5(256.42) + var generatedHash = persistenceStore.GenerateHash(new JValue(256.42)); + generatedHash.Should().Be(expectedHash); + } + + private static APIGatewayProxyRequest LoadApiGatewayProxyRequest() + { + var eventJson = File.ReadAllText("./resources/apigw_event.json"); + var request = JsonConvert.DeserializeObject(eventJson); + return request!; + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs new file mode 100644 index 00000000..c4af3232 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs @@ -0,0 +1,71 @@ +/* + * 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 AWS.Lambda.Powertools.Idempotency.Persistence; +using FluentAssertions; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; + +public class DataRecordTests +{ + [Fact] + public void IsExpired_ShouldReturnTrue_WhenCurrentTimeIsGreaterThanExpiryTimestamp() + { + var now = DateTimeOffset.UtcNow; + var dataRecord = new DataRecord( + "123", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(-1).ToUnixTimeSeconds(), + "abc","123"); + dataRecord.IsExpired(now).Should().BeTrue(); + } + [Fact] + public void IsExpired_ShouldReturnFalse_WhenCurrentTimeIsLessThanExpiryTimestamp() + { + var now = DateTimeOffset.UtcNow; + var dataRecord = new DataRecord( + "123", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(10).ToUnixTimeSeconds(), + "abc","123"); + dataRecord.IsExpired(now).Should().BeFalse(); + } + + [Fact] + public void Status_ShouldBeExpired_WhenCurrentTimeIsGreaterThanExpiryTimestamp() + { + var now = DateTimeOffset.UtcNow; + var dataRecord = new DataRecord( + "123", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(-10).ToUnixTimeSeconds(), + "abc","123"); + dataRecord.Status.Should().Be(DataRecord.DataRecordStatus.EXPIRED); + } + + [Fact] + public void Status_ShouldBeRecordStatus_WhenCurrentTimeDidnotExpire() + { + var now = DateTimeOffset.UtcNow; + var dataRecord = new DataRecord( + "123", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(10).ToUnixTimeSeconds(), + "abc","123"); + dataRecord.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs new file mode 100644 index 00000000..33aa8ee9 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -0,0 +1,348 @@ +/* + * 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.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Persistence; +using FluentAssertions; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; + +public class DynamoDBPersistenceStoreTests : IntegrationTestBase +{ + private DynamoDBPersistenceStore _dynamoDbPersistenceStore; + + public override async Task InitializeAsync() + { + await base.InitializeAsync(); + _dynamoDbPersistenceStore = DynamoDBPersistenceStore + .Builder() + .WithTableName(TABLE_NAME) + .WithDynamoDBClient(client) + .Build(); + _dynamoDbPersistenceStore.Configure(IdempotencyConfig.Builder().Build(),functionName: null); + } + //putRecord + [Fact] + public async Task PutRecord_ShouldCreateRecordInDynamoDB() + { + DateTimeOffset now = DateTimeOffset.UtcNow; + long expiry = now.AddSeconds(3600).ToUnixTimeSeconds(); + await _dynamoDbPersistenceStore + .PutRecord(new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, null, null), now); + + var key = CreateKey("key"); + + var getItemResponse = + await client.GetItemAsync(new GetItemRequest + { + TableName = TABLE_NAME, + Key = key + }); + + var item = getItemResponse.Item; + item.Should().NotBeNull(); + item["status"].S.Should().Be("COMPLETED"); + item["expiration"].N.Should().Be(expiry.ToString()); + } + + [Fact] + public async Task PutRecord_ShouldThrowIdempotencyItemAlreadyExistsException_IfRecordAlreadyExist() + { + var key = CreateKey("key"); + + // GIVEN: Insert a fake item with same id + Dictionary item = new(key); + DateTimeOffset now = DateTimeOffset.UtcNow; + long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue(){N = expiry.ToString()}); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); + item.Add("data", new AttributeValue("Fake Data")); + await client.PutItemAsync(new PutItemRequest + { + TableName = TABLE_NAME, + Item = item + }); + + // WHEN: call putRecord + long expiry2 = now.AddSeconds(3600).ToUnixTimeSeconds(); + Func act = () => _dynamoDbPersistenceStore.PutRecord( + new DataRecord("key", + DataRecord.DataRecordStatus.INPROGRESS, + expiry2, + null, + null + ), now); + await act.Should().ThrowAsync(); + + // THEN: item was not updated, retrieve the initial one + Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest + { + TableName = TABLE_NAME, + Key = key + })).Item; + itemInDb.Should().NotBeNull(); + itemInDb["status"].S.Should().Be("COMPLETED"); + itemInDb["expiration"].N.Should().Be(expiry.ToString()); + itemInDb["data"].S.Should().Be("Fake Data"); + } + + //getRecord + [Fact] + public async Task GetRecord_ShouldReturnExistingRecord() + { + var key = new DictionaryEntry(); + // GIVEN: Insert a fake item with same id + Dictionary item = new() + { + {"id", new AttributeValue("key")} //key + }; + DateTimeOffset now = DateTimeOffset.UtcNow; + long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue + { + N = expiry.ToString() + }); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); + item.Add("data", new AttributeValue("Fake Data")); + var response = await client.PutItemAsync(new PutItemRequest() + { + TableName = TABLE_NAME, + Item = item + }); + + // WHEN + DataRecord record = await _dynamoDbPersistenceStore.GetRecord("key"); + + // THEN + record.IdempotencyKey.Should().Be("key"); + record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); + record.ResponseData.Should().Be("Fake Data"); + record.ExpiryTimestamp.Should().Be(expiry); + } + + [Fact] + public async Task GetRecord_ShouldThrowException_WhenRecordIsAbsent() + { + Func act = () => _dynamoDbPersistenceStore.GetRecord("key"); + await act.Should().ThrowAsync(); + } + //updateRecord + + [Fact] + public async Task UpdateRecord_ShouldUpdateRecord() + { + // GIVEN: Insert a fake item with same id + var key = CreateKey("key"); + Dictionary item = new(key); + DateTimeOffset now = DateTimeOffset.UtcNow; + long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue + { + N = expiry.ToString() + }); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); + await client.PutItemAsync(new PutItemRequest + { + TableName = TABLE_NAME, + Item = item + }); + // enable payload validation + _dynamoDbPersistenceStore.Configure(IdempotencyConfig.Builder().WithPayloadValidationJmesPath("path").Build(), + null); + + // WHEN + expiry = now.AddSeconds(3600).ToUnixTimeMilliseconds(); + DataRecord record = new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, "Fake result", "hash"); + await _dynamoDbPersistenceStore.UpdateRecord(record); + + // THEN + Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest + { + TableName = TABLE_NAME, + Key = key + })).Item; + + itemInDb["status"].S.Should().Be("COMPLETED"); + itemInDb["expiration"].N.Should().Be(expiry.ToString()); + itemInDb["data"].S.Should().Be("Fake result"); + itemInDb["validation"].S.Should().Be("hash"); + } + + //deleteRecord + [Fact] + public async Task DeleteRecord_ShouldDeleteRecord() + { + // GIVEN: Insert a fake item with same id + var key = CreateKey("key"); + Dictionary item = new(key); + DateTimeOffset now = DateTimeOffset.UtcNow; + long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue(){N=expiry.ToString()}); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); + await client.PutItemAsync(new PutItemRequest + { + TableName = TABLE_NAME, + Item = item + }); + var scanResponse = await client.ScanAsync(new ScanRequest + { + TableName = TABLE_NAME + }); + scanResponse.Items.Count.Should().Be(1); + + // WHEN + await _dynamoDbPersistenceStore.DeleteRecord("key"); + + // THEN + scanResponse = await client.ScanAsync(new ScanRequest + { + TableName = TABLE_NAME + }); + scanResponse.Items.Count.Should().Be(0); + } + + [Fact] + public async Task EndToEndWithCustomAttrNamesAndSortKey() + { + var TABLE_NAME_CUSTOM = "idempotency_table_custom"; + try + { + var createTableRequest = new CreateTableRequest + { + TableName = TABLE_NAME_CUSTOM, + KeySchema = new List() + { + new KeySchemaElement("key", KeyType.HASH), + new KeySchemaElement("sortkey", KeyType.RANGE) + }, + AttributeDefinitions = new List() + { + new AttributeDefinition("key", ScalarAttributeType.S), + new AttributeDefinition("sortkey", ScalarAttributeType.S) + }, + BillingMode = BillingMode.PAY_PER_REQUEST + }; + await client.CreateTableAsync(createTableRequest); + DynamoDBPersistenceStore persistenceStore = DynamoDBPersistenceStore.Builder() + .WithTableName(TABLE_NAME_CUSTOM) + .WithDynamoDBClient(client) + .WithDataAttr("result") + .WithExpiryAttr("expiry") + .WithKeyAttr("key") + .WithSortKeyAttr("sortkey") + .WithStaticPkValue("pk") + .WithStatusAttr("state") + .WithValidationAttr("valid") + .Build(); + persistenceStore.Configure(IdempotencyConfig.Builder().Build(),functionName: null); + + DateTimeOffset now = DateTimeOffset.UtcNow; + DataRecord record = new DataRecord( + "mykey", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(400).ToUnixTimeMilliseconds(), + null, + null + ); + // PUT + await persistenceStore.PutRecord(record, now); + + Dictionary customKey = new(); + customKey.Add("key", new AttributeValue("pk")); + customKey.Add("sortkey", new AttributeValue("mykey")); + + Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest + { + TableName = TABLE_NAME_CUSTOM, + Key = customKey + })).Item; + + // GET + DataRecord recordInDb = await persistenceStore.GetRecord("mykey"); + + itemInDb.Should().NotBeNull(); + itemInDb["key"].S.Should().Be("pk"); + itemInDb["sortkey"].S.Should().Be(recordInDb.IdempotencyKey); + itemInDb["state"].S.Should().Be(recordInDb.Status.ToString()); + itemInDb["expiry"].N.Should().Be(recordInDb.ExpiryTimestamp.ToString()); + + // UPDATE + DataRecord updatedRecord = new DataRecord( + "mykey", + DataRecord.DataRecordStatus.COMPLETED, + now.AddSeconds(500).ToUnixTimeMilliseconds(), + "response", + null + ); + await persistenceStore.UpdateRecord(updatedRecord); + recordInDb = await persistenceStore.GetRecord("mykey"); + recordInDb.Should().Be(updatedRecord); + + // DELETE + await persistenceStore.DeleteRecord("mykey"); + (await client.ScanAsync(new ScanRequest + { + TableName = TABLE_NAME_CUSTOM + })).Count.Should().Be(0); + + } + finally + { + try + { + await client.DeleteTableAsync(new DeleteTableRequest + { + TableName = TABLE_NAME_CUSTOM + }); + } + catch (Exception) + { + // OK + } + } + } + + [Fact] + public async Task IdempotencyDisabled_NoClientShouldBeCreated() + { + try + { + Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); + + DynamoDBPersistenceStore store = DynamoDBPersistenceStore.Builder().WithTableName(TABLE_NAME).Build(); + Func act = () => store.GetRecord("fake"); + await act.Should().ThrowAsync(); + } + finally + { + Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "false"); + } + } + private static Dictionary CreateKey(string keyValue) + { + var key = new Dictionary() + { + {"id", new AttributeValue(keyValue)} + }; + return key; + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event.json b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event.json new file mode 100644 index 00000000..cf3f372e --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"message\": \"Lambda rocks\", \"id\": 43876123454654}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event2.json b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event2.json new file mode 100644 index 00000000..a313815c --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/resources/apigw_event2.json @@ -0,0 +1,62 @@ +{ + "body": "{\"address\": \"https://checkip.amazonaws.com\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} From 89c680a48d0e9860632258822b2fad9e223dbb48 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Tue, 23 Aug 2022 08:51:07 +0400 Subject: [PATCH 02/32] Add documentation --- docs/core/idempotency.md | 451 ++++++++++++++++++ .../README.md | 39 ++ 2 files changed, 490 insertions(+) create mode 100644 docs/core/idempotency.md create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/README.md diff --git a/docs/core/idempotency.md b/docs/core/idempotency.md new file mode 100644 index 00000000..2c9c095c --- /dev/null +++ b/docs/core/idempotency.md @@ -0,0 +1,451 @@ +--- +title: Idempotency +description: Utility +--- + +The idempotency utility provides a simple solution to convert your Lambda functions into idempotent operations which +are safe to retry. + +## Terminology + +The property of idempotency means that an operation does not cause additional side effects if it is called more than +once with the same input parameters. + +**Idempotent operations will return the same result when they are called multiple +times with the same parameters**. This makes idempotent operations safe to retry. [Read more](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/) about idempotency. + +**Idempotency key** is a hash representation of either the entire event or a specific configured subset of the event, and invocation results are **JSON serialized** and stored in your persistence storage layer. + +## Key features + +* Prevent Lambda handler function from executing more than once on the same event payload during a time window +* Ensure Lambda handler returns the same result when called with the same payload +* Select a subset of the event as the idempotency key using JMESPath expressions +* Set a time window in which records with the same payload should be considered duplicates + +## Getting started + +### Installation +You should install with NuGet: + +``` +Install-Package Amazon.Lambda.PowerTools.Idempotency +``` + +Or via the .NET Core command line interface: + +``` +dotnet add package Amazon.Lambda.PowerTools.Idempotency +``` + +### Required resources + +Before getting started, you need to create a persistent storage layer where the idempotency utility can store its state - your Lambda functions will need read and write access to it. + +As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first. + +**Default table configuration** + +If you're not [changing the default configuration for the DynamoDB persistence layer](#dynamodbpersistencestore), this is the expected default configuration: + +| Configuration | Value | Notes | +|--------------------|--------------|-------------------------------------------------------------------------------------| +| Partition key | `id` | | +| TTL attribute name | `expiration` | This can only be configured after your table is created if you're using AWS Console | + +!!! Tip "Tip: You can share a single state table for all functions" + You can reuse the same DynamoDB table to store idempotency state. We add your function name in addition to the idempotency key as a hash key. + +```yaml hl_lines="5-13 21-23 26" title="AWS Serverless Application Model (SAM) example" +Resources: + IdempotencyTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + TimeToLiveSpecification: + AttributeName: expiration + Enabled: true + BillingMode: PAY_PER_REQUEST + + IdempotencyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: Function + Handler: HelloWorld::HelloWorld.Function::FunctionHandler + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref IdempotencyTable + Environment: + Variables: + IDEMPOTENCY_TABLE: !Ref IdempotencyTable +``` + +!!! warning "Warning: Large responses with DynamoDB persistence layer" + When using this utility with DynamoDB, your function's responses must be [smaller than 400KB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-items). + Larger items cannot be written to DynamoDB and will cause exceptions. + +!!! info "Info: DynamoDB" + Each function invocation will generally make 2 requests to DynamoDB. If the + result returned by your Lambda is less than 1kb, you can expect 2 WCUs per invocation. For retried invocations, you will + see 1WCU and 1RCU. Review the [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/) to + estimate the cost. + +### Idempotent attribute + +You can quickly start by initializing the `DynamoDBPersistenceStore` and using it with the `Idempotent` attribute on your Lambda function. + +!!! warning "Important" + Initialization and configuration of the `DynamoDBPersistenceStore` must be performed outside the handler, preferably in the constructor. + + ```csharp + public class Function + { + public Function() + { + Idempotency.Config() + .WithConfig( + IdempotencyConfig.Builder() + .Build()) + .WithPersistenceStore( + DynamoDBPersistenceStore.Builder() + .WithTableName("Lambda_Is_Cool") + .Build() + ).Configure(); + } + + [Idempotent] + public Task FunctionHandler(string input, ILambdaContext context) + { + return Task.FromResult(input.ToUpper()); + } + } + ``` + +### Choosing a payload subset for idempotency + +!!! tip "Tip: Dealing with always changing payloads" + When dealing with an elaborate payload (API Gateway request for example), where parts of the payload always change, you should configure the **`EventKeyJmesPath`**. + +Use [`IdempotencyConfig`](#customizing-the-default-behavior) to instruct the Idempotent annotation to only use a portion of your payload to verify whether a request is idempotent, and therefore it should not be retried. + +> **Payment scenario** + +In this example, we have a Lambda handler that creates a payment for a user subscribing to a product. We want to ensure that we don't accidentally charge our customer by subscribing them more than once. + +Imagine the function executes successfully, but the client never receives the response due to a connection issue. It is safe to retry in this instance, as the idempotent decorator will return a previously saved response. + +!!! warning "Warning: Idempotency for JSON payloads" + The payload extracted by the `EventKeyJmesPath` is treated as a string by default, so will be sensitive to differences in whitespace even when the JSON payload itself is identical. + + To alter this behaviour, you can use the [JMESPath built-in function](utilities.md#powertools_json-function) `powertools_json()` to treat the payload as a JSON object rather than a string. + + ```csharp + Idempotency.Config() + .WithConfig( + IdempotencyConfig.Builder() + .Build()) + .WithPersistenceStore( + DynamoDBPersistenceStore.Builder() + .WithTableName("Lambda_Is_Cool") + .Build() + ).Configure(); + ``` + +### Handling exceptions + +If you are using the `Idempotent` attribute on your Lambda handler or any other method, any unhandled exceptions that are thrown during the code execution will cause **the record in the persistence layer to be deleted**. +This means that new invocations will execute your code again despite having the same payload. If you don't want the record to be deleted, you need to catch exceptions within the idempotent function and return a successful response. + +!!! warning + **We will throw an `IdempotencyPersistenceLayerException`** if any of the calls to the persistence layer fail unexpectedly. + + As this happens outside the scope of your decorated function, you are not able to catch it. + +### Persistence stores + +#### DynamoDBPersistenceStore + +This persistence store is built-in, and you can either use an existing DynamoDB table or create a new one dedicated for idempotency state (recommended). + +Use the builder to customize the table structure: +```csharp title="Customizing DynamoDBPersistenceStore to suit your table structure" +DynamoDBPersistenceStore.Builder() + .WithTableName(System.getenv("TABLE_NAME")) + .WithKeyAttr("idempotency_key") + .WithExpiryAttr("expires_at") + .WithStatusAttr("current_status") + .WithDataAttr("result_data") + .WithValidationAttr("validation_key") + .Build() +``` + +When using DynamoDB as a persistence layer, you can alter the attribute names by passing these parameters when initializing the persistence layer: + +| Parameter | Required | Default | Description | +|--------------------|----------|--------------------------------------|--------------------------------------------------------------------------------------------------------| +| **TableName** | Y | | Table name to store state | +| **KeyAttr** | | `id` | Partition key of the table. Hashed representation of the payload (unless **SortKeyAttr** is specified) | +| **ExpiryAttr** | | `expiration` | Unix timestamp of when record expires | +| **StatusAttr** | | `status` | Stores status of the Lambda execution during and after invocation | +| **DataAttr** | | `data` | Stores results of successfully idempotent methods | +| **ValidationAttr** | | `validation` | Hashed representation of the parts of the event used for validation | +| **SortKeyAttr** | | | Sort key of the table (if table is configured with a sort key). | +| **StaticPkValue** | | `idempotency#{LAMBDA_FUNCTION_NAME}` | Static value to use as the partition key. Only used when **SortKeyAttr** is set. | + +## Advanced + +### Customizing the default behavior + +Idempotency behavior can be further configured with **`IdempotencyConfig`** using a builder: + +```csharp +IdempotencyConfig.Builder() + .WithEventKeyJmesPath("id") + .WithPayloadValidationJmesPath("paymentId") + .WithThrowOnNoIdempotencyKey(true) + .WithExpiration(TimeSpan.FromMinutes(1)) + .WithUseLocalCache(true) + .WithHashFunction("MD5") + .Build(); +``` + +These are the available options for further configuration: + +| Parameter | Default | Description | +|---------------------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------| +| **EventKeyJMESPath** | `""` | JMESPath expression to extract the idempotency key from the event record. See available [built-in functions](serialization) | +| **PayloadValidationJMESPath** | `""` | JMESPath expression to validate whether certain parameters have changed in the event | +| **ThrowOnNoIdempotencyKey** | `False` | Throw exception if no idempotency key was found in the request | +| **ExpirationInSeconds** | 3600 | The number of seconds to wait before a record is expired | +| **UseLocalCache** | `false` | Whether to locally cache idempotency results (LRU cache) | +| **HashFunction** | `MD5` | Algorithm to use for calculating hashes, as supported by `java.security.MessageDigest` (eg. SHA-1, SHA-256, ...) | + +These features are detailed below. + +### Handling concurrent executions with the same payload + +This utility will throw an **`IdempotencyAlreadyInProgressException`** if we receive **multiple invocations with the same payload while the first invocation hasn't completed yet**. + +!!! info + If you receive `IdempotencyAlreadyInProgressException`, you can safely retry the operation. + +This is a locking mechanism for correctness. Since we don't know the result from the first invocation yet, we can't safely allow another concurrent execution. + +### Using in-memory cache + +**By default, in-memory local caching is disabled**, to avoid using memory in an unpredictable way. + +!!! warning Memory configuration of your function + Be sure to configure the Lambda memory according to the number of records and the potential size of each record. + +You can enable it as seen before with: +```csharp title="Enable local cache" + IdempotencyConfig.Builder() + .WithUseLocalCache(true) + .Build() +``` +When enabled, we cache a maximum of 255 records in each Lambda execution environment + +!!! note "Note: This in-memory cache is local to each Lambda execution environment" + This means it will be effective in cases where your function's concurrency is low in comparison to the number of "retry" invocations with the same payload, because cache might be empty. + + +### Expiring idempotency records + +!!! note + By default, we expire idempotency records after **an hour** (3600 seconds). + +In most cases, it is not desirable to store the idempotency records forever. Rather, you want to guarantee that the same payload won't be executed within a period of time. + +You can change this window with the **`ExpirationInSeconds`** parameter: +```csharp title="Customizing expiration time" +IdempotencyConfig.Builder() + .WithExpiration(TimeSpan.FromMinutes(5)) + .Build() +``` + +Records older than 5 minutes will be marked as expired, and the Lambda handler will be executed normally even if it is invoked with a matching payload. + +!!! note "Note: DynamoDB time-to-live field" + This utility uses **`expiration`** as the TTL field in DynamoDB, as [demonstrated in the SAM example earlier](#required-resources). + +### Payload validation + +!!! question "Question: What if your function is invoked with the same payload except some outer parameters have changed?" + Example: A payment transaction for a given productID was requested twice for the same customer, **however the amount to be paid has changed in the second transaction**. + +By default, we will return the same result as it returned before, however in this instance it may be misleading; we provide a fail fast payload validation to address this edge case. + +With **`PayloadValidationJMESPath`**, you can provide an additional JMESPath expression to specify which part of the event body should be validated against previous idempotent invocations + +=== "App.java" + + ```csharp + Idempotency.Config() + .WithPersistenceStore(DynamoDBPersistenceStore.Builder() + .WithTableName("TABLE_NAME") + .Build()) + .WithConfig(IdempotencyConfig.Builder() + .WithEventKeyJmesPath("[userDetail, productId]") + .WithPayloadValidationJmesPath("amount") + .Build()) + .Configure(); + ``` + +=== "Example Event 1" + + ```json hl_lines="8" + { + "userDetail": { + "username": "User1", + "user_email": "user@example.com" + }, + "productId": 1500, + "charge_type": "subscription", + "amount": 500 + } + ``` + +=== "Example Event 2" + + ```json hl_lines="8" + { + "userDetail": { + "username": "User1", + "user_email": "user@example.com" + }, + "productId": 1500, + "charge_type": "subscription", + "amount": 1 + } + ``` + +In this example, the **`userDetail`** and **`productId`** keys are used as the payload to generate the idempotency key, as per **`EventKeyJMESPath`** parameter. + +!!! note + If we try to send the same request but with a different amount, we will raise **`IdempotencyValidationException`**. + +Without payload validation, we would have returned the same result as we did for the initial request. Since we're also returning an amount in the response, this could be quite confusing for the client. + +By using **`withPayloadValidationJMESPath("amount")`**, we prevent this potentially confusing behavior and instead throw an Exception. + +### Making idempotency key required + +If you want to enforce that an idempotency key is required, you can set **`ThrowOnNoIdempotencyKey`** to `True`. + +This means that we will throw **`IdempotencyKeyException`** if the evaluation of **`EventKeyJMESPath`** is `null`. + +=== "Function.cs" + + ```csharp + public App() + { + Idempotency.Config() + .WithPersistenceStore(DynamoDBPersistenceStore.Builder() + .WithTableName("TABLE_NAME") + .Build()) + .WithConfig(IdempotencyConfig.Builder() + // Requires "user"."uid" and "orderId" to be present + .WithEventKeyJmesPath("[user.uid, orderId]") + .WithThrowOnNoIdempotencyKey(true) + .Build()) + .Configure(); + } + + [Idempotent] + public Task FunctionHandler(Order input, ILambdaContext context) + { + // ... + } + ``` + +=== "Success Event" + + ```json + { + "user": { + "uid": "BB0D045C-8878-40C8-889E-38B3CB0A61B1", + "name": "Foo" + }, + "orderId": 10000 + } + ``` + +=== "Failure Event" + + Notice that `orderId` is now accidentally within `user` key + + ```json + { + "user": { + "uid": "DE0D000E-1234-10D1-991E-EAC1DD1D52C8", + "name": "Joe Bloggs", + "orderId": 10000 + }, + } + ``` + +### Customizing DynamoDB configuration + +When creating the `DynamoDBPersistenceStore`, you can set a custom [`AmazonDynamoDBClient`](https://docs.aws.amazon.com/sdkfornet1/latest/apidocs/html/T_Amazon_DynamoDB_AmazonDynamoDBClient.htm) if you need to customize the configuration: + +=== "Custom AmazonDynamoDBClient" + + ```csharp + public Function() + { + AmazonDynamoDBClient customClient = new AmazonDynamoDBClient(RegionEndpoint.APSouth1); + + Idempotency.Config().WithPersistenceStore( + DynamoDBPersistenceStore.Builder() + .WithTableName("TABLE_NAME") + .WithDynamoDBClient(customClient) + .Build() + ).Configure(); + } + ``` + +### Using a DynamoDB table with a composite primary key + +When using a composite primary key table (hash+range key), use `SortKeyAttr` parameter when initializing your persistence store. + +With this setting, we will save the idempotency key in the sort key instead of the primary key. By default, the primary key will now be set to `idempotency#{LAMBDA_FUNCTION_NAME}`. + +You can optionally set a static value for the partition key using the `StaticPkValue` parameter. + +```csharp title="Reusing a DynamoDB table that uses a composite primary key" +Idempotency.Config() + .WithPersistenceStore( + DynamoDBPersistenceStore.Builder() + .WithTableName(("TABLE_NAME")) + .WithSortKeyAttr("sort_key") + .Build()) + .Configure(); +``` + +Data would then be stored in DynamoDB like this: + +| id | sort_key | expiration | status | data | +|------------------------------|----------------------------------|------------|-------------|--------------------------------------| +| idempotency#MyLambdaFunction | 1e956ef7da78d0cb890be999aecc0c9e | 1636549553 | COMPLETED | {"id": 12391, "message": "success"} | +| idempotency#MyLambdaFunction | 2b2cdb5f86361e97b4383087c1ffdf27 | 1636549571 | COMPLETED | {"id": 527212, "message": "success"} | +| idempotency#MyLambdaFunction | f091d2527ad1c78f05d54cc3f363be80 | 1636549585 | IN_PROGRESS | | + +## Compatibility with other utilities + +## Testing your code + +The idempotency utility provides several routes to test your code. + +### 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. + +## Extra resources + +If you're interested in a deep dive on how Amazon uses idempotency when building our APIs, check out +[this article](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/). diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/README.md b/libraries/src/AWS.Lambda.Powertools.Idempotency/README.md new file mode 100644 index 00000000..2ebf57fc --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/README.md @@ -0,0 +1,39 @@ +# AWS Lambda Idempotency for .NET + +The idempotency package provides a simple solution to convert your Lambda functions into idempotent operations which +are safe to retry. + +## Terminology + +The property of idempotency means that an operation does not cause additional side effects if it is called more than +once with the same input parameters. + +**Idempotent operations will return the same result when they are called multiple +times with the same parameters**. This makes idempotent operations safe to retry. [Read more](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/) about idempotency. + +**Idempotency key** is a hash representation of either the entire event or a specific configured subset of the event, and invocation results are **JSON serialized** and stored in your persistence storage layer. + + +## Key features + +* Prevent Lambda handler function from executing more than once on the same event payload during a time window +* Ensure Lambda handler returns the same result when called with the same payload +* Select a subset of the event as the idempotency key using JMESPath expressions +* Set a time window in which records with the same payload should be considered duplicates + + +## Installation +You should install with NuGet: + +``` +Install-Package Amazon.Lambda.PowerTools.Idempotency +``` + +Or via the .NET Core command line interface: + +``` +dotnet add package Amazon.Lambda.PowerTools.Idempotency +``` + +## Acknowledgment +This project has been ported from the Java Idempotency PowerTool Utility From 7abb323f6cc5549b8f2414307b2d288a88ce63e7 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Tue, 23 Aug 2022 21:21:52 +0400 Subject: [PATCH 03/32] Consistent project files --- .../AWS.Lambda.Powertools.Idempotency.csproj | 36 +++++++++++++++++-- .../IdempotencyAlreadyInProgressException.cs | 2 ++ .../IdempotencyConfigurationException.cs | 2 ++ .../IdempotencyInconsistentStateException.cs | 2 ++ .../IdempotencyItemAlreadyExistsException.cs | 2 ++ .../IdempotencyItemNotFoundException.cs | 2 ++ .../Exceptions/IdempotencyKeyException.cs | 2 ++ .../IdempotencyPersistenceLayerException.cs | 2 ++ .../IdempotencyValidationException.cs | 2 ++ .../Idempotency.cs | 1 + .../IdempotencyConfig.cs | 1 + .../IdempotentAttribute.cs | 3 +- .../Internal/IdempotencyHandler.cs | 2 ++ .../Internal/IdempotentAspect.cs | 2 ++ .../Internal/LRUCache.cs | 2 ++ .../Output/ConsoleLog.cs | 2 ++ .../Persistence/BasePersistenceStore.cs | 2 ++ .../Persistence/DataRecord.cs | 2 ++ .../Persistence/DynamoDBPersistenceStore.cs | 3 ++ .../Persistence/IPersistenceStore.cs | 2 ++ ...Lambda.Powertools.Idempotency.Tests.csproj | 6 ++-- 21 files changed, 73 insertions(+), 7 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index a9c56e61..be35e5a7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -2,17 +2,47 @@ net6.0 - enable - enable + default + AWS.Lambda.Powertools.Idempotency 0.0.1 + enable + Amazon Web Services + Amazon.com, Inc + AWS Lambda Powertools for .NET + AWS Lambda Powertools for .NET - Logging package. + Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + https://github.com/awslabs/aws-lambda-powertools-dotnet + Apache-2.0 + AWS;Amazon;Lambda;Powertools + README.md + https://sdk-for-net.amazonwebservices.com/images/AWSLogo128x128.png + AWSLogo128x128.png + true + AWS.Lambda.Powertools.Idempotency + AWS.Lambda.Powertools.Idempotency + + true + + + + + + - + + + + + + + + diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs index 7d8aefa2..d52cb4f8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyAlreadyInProgressException: Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs index ccb838d9..078e3f60 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyConfigurationException : Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs index 5d3257c9..9267563a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyInconsistentStateException : Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs index d4fe7de0..eda8ad2c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyItemAlreadyExistsException : Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs index f1cdbd0a..bf5f7d3b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyItemNotFoundException : Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs index 6c57dcea..1bef3078 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyKeyException : Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs index 49bc25eb..b288ecac 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyPersistenceLayerException : Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs index e4a3aea2..8ec387a4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Exceptions; public class IdempotencyValidationException : Exception diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index d7ffa44e..94c20168 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using System; using AWS.Lambda.Powertools.Idempotency.Persistence; namespace AWS.Lambda.Powertools.Idempotency; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs index 3495b5df..3bafecd2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using System; using AWS.Lambda.Powertools.Idempotency.Output; namespace AWS.Lambda.Powertools.Idempotency; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 9bbe09a9..52f3f529 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -13,12 +13,13 @@ * permissions and limitations under the License. */ +using System; using AspectInjector.Broker; using AWS.Lambda.Powertools.Idempotency.Internal; namespace AWS.Lambda.Powertools.Idempotency; -[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] [Injection(typeof(IdempotentAspect), Inherited = true)] public class IdempotentAttribute : Attribute { diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs index c5ad3c16..d03dfc23 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; +using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Output; using AWS.Lambda.Powertools.Idempotency.Persistence; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs index d7be68b8..93daa37d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs @@ -13,7 +13,9 @@ * permissions and limitations under the License. */ +using System; using System.Reflection; +using System.Threading.Tasks; using AspectInjector.Broker; using AWS.Lambda.Powertools.Idempotency.Exceptions; using Newtonsoft.Json.Linq; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs index c7b769c1..88d1e86c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs @@ -1,6 +1,8 @@ #nullable disable +using System.Collections.Generic; + namespace AWS.Lambda.Powertools.Idempotency.Internal; //source: https://github.dev/microsoft/botbuilder-dotnet/blob/main/libraries/AdaptiveExpressions/LRUCache.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs index 1bffc78c..2745a35b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Output; /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 483e3a3c..f090d2ed 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -13,8 +13,10 @@ * permissions and limitations under the License. */ +using System; using System.Security.Cryptography; using System.Text; +using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; using AWS.Lambda.Powertools.Idempotency.Output; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs index 08c0afec..3008c2ce 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; + namespace AWS.Lambda.Powertools.Idempotency.Persistence; public class DataRecord diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index a9d43ff7..7925f1e3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -13,6 +13,9 @@ * permissions and limitations under the License. */ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; using Amazon; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs index 978a62d1..7e00371c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/IPersistenceStore.cs @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +using System; +using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; namespace AWS.Lambda.Powertools.Idempotency.Persistence; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj index 205ba897..249b81e6 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj @@ -2,9 +2,9 @@ net6.0 - - false - + default + AWS.Lambda.Powertools.Idempotency.Tests + AWS.Lambda.Powertools.Idempotency.Tests From eee318c739558cfa02994070269e9083f4cd7b7c Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Wed, 7 Sep 2022 21:11:05 +0400 Subject: [PATCH 04/32] Update test names to follow the convention __() --- .../Internal/IdempotentAspectTests.cs | 92 ++++++++------- .../{LRUCacheTest.cs => LRUCacheTests.cs} | 2 +- .../Model/Product.cs | 13 +-- .../Persistence/BasePersistenceStoreTests.cs | 106 ++++++++++++++---- .../Persistence/DataRecordTests.cs | 40 +++++-- .../DynamoDBPersistenceStoreTests.cs | 62 ++++++---- 6 files changed, 212 insertions(+), 103 deletions(-) rename libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/{LRUCacheTest.cs => LRUCacheTests.cs} (99%) diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index f437a41e..b0580c03 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -30,10 +30,11 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; public class IdempotentAspectTests { [Fact] - public async Task FirstCall_ShouldPutInStore() + public async Task Handle_WhenFirstCall_ShouldPutInStore() { + //Arrange var store = new Mock(); - AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + Idempotency.Config() .WithPersistenceStore(store.Object) .WithConfig(IdempotencyConfig.Builder() .WithEventKeyJmesPath("Id") @@ -41,60 +42,66 @@ public async Task FirstCall_ShouldPutInStore() ).Configure(); IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); - - Product p = new Product(42, "fake product", 12); - Basket basket = await function.Handle(p, new TestLambdaContext()); + Product product = new Product(42, "fake product", 12); + + //Act + Basket basket = await function.Handle(product, new TestLambdaContext()); + + //Assert basket.Products.Count.Should().Be(1); function.HandlerExecuted.Should().BeTrue(); store - .Verify(x=>x.SaveInProgress(It.Is(t=> t.ToString() == JToken.FromObject(p).ToString()), It.IsAny())); + .Verify(x=>x.SaveInProgress(It.Is(t=> t.ToString() == JToken.FromObject(product).ToString()), It.IsAny())); store - .Verify(x=>x.SaveSuccess(It.IsAny(), It.Is(x => x.Equals(basket)), It.IsAny())); + .Verify(x=>x.SaveSuccess(It.IsAny(), It.Is(y => y.Equals(basket)), It.IsAny())); } [Fact] - public async Task SecondCall_NotExpired_ShouldGetFromStore() + public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() { + //Arrange var store = new Mock(); store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) .Throws(); // GIVEN - AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + Idempotency.Config() .WithPersistenceStore(store.Object) .WithConfig(IdempotencyConfig.Builder() .WithEventKeyJmesPath("Id") .Build() ).Configure(); - Product p = new Product(42, "fake product", 12); - Basket b = new Basket(p); + Product product = new Product(42, "fake product", 12); + Basket basket = new Basket(product); DataRecord record = new DataRecord( "42", DataRecord.DataRecordStatus.COMPLETED, DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), - JToken.FromObject(b).ToString(), + JToken.FromObject(basket).ToString(), null); store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); - // WHEN IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); - Basket basket = await function.Handle(p, new TestLambdaContext()); - // THEN - basket.Should().Be(b); + // Act + Basket resultBasket = await function.Handle(product, new TestLambdaContext()); + + // Assert + resultBasket.Should().Be(basket); function.HandlerExecuted.Should().BeFalse(); } [Fact] - public async Task SecondCall_InProgress_ShouldThrowIdempotencyAlreadyInProgressException() + public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempotencyAlreadyInProgressException() { + // Arrange var store = new Mock(); - // GIVEN - AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + + Idempotency.Config() .WithPersistenceStore(store.Object) .WithConfig(IdempotencyConfig.Builder() .WithEventKeyJmesPath("Id") @@ -103,70 +110,73 @@ public async Task SecondCall_InProgress_ShouldThrowIdempotencyAlreadyInProgressE store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) .Throws(); - Product p = new Product(42, "fake product", 12); - Basket b = new Basket(p); + Product product = new Product(42, "fake product", 12); + Basket basket = new Basket(product); DataRecord record = new DataRecord( "42", DataRecord.DataRecordStatus.INPROGRESS, DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), - JToken.FromObject(b).ToString(), + JToken.FromObject(basket).ToString(), null); store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); - // THEN + // Act IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); - Func act = async () => await function.Handle(p, new TestLambdaContext()); + Func act = async () => await function.Handle(product, new TestLambdaContext()); + // Assert await act.Should().ThrowAsync(); - } [Fact] - public async Task FunctionThrowException_ShouldDeleteRecord_AndThrowFunctionException() + public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionException() { + // Arrange var store = new Mock(); - // GIVEN - AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + + Idempotency.Config() .WithPersistenceStore(store.Object) .WithConfig(IdempotencyConfig.Builder() .WithEventKeyJmesPath("Id") .Build() ).Configure(); - - // WHEN / THEN + IdempotencyWithErrorFunction function = new IdempotencyWithErrorFunction(); + Product product = new Product(42, "fake product", 12); - Product p = new Product(42, "fake product", 12); - Func act = async () => await function.Handle(p, new TestLambdaContext()); + // Act + Func act = async () => await function.Handle(product, new TestLambdaContext()); + // Assert await act.Should().ThrowAsync(); - store.Verify( x => x.DeleteRecord(It.IsAny(), It.IsAny())); } [Fact] - public async Task TestIdempotencyDisabled_ShouldJustRunTheFunction() + public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() { try { + // Arrange var store = new Mock(); Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); - // GIVEN - AWS.Lambda.Powertools.Idempotency.Idempotency.Config() + + Idempotency.Config() .WithPersistenceStore(store.Object) .WithConfig(IdempotencyConfig.Builder() .WithEventKeyJmesPath("Id") .Build() ).Configure(); - - // WHEN + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); - Product p = new Product(42, "fake product", 12); - Basket basket = await function.Handle(p, new TestLambdaContext()); + Product product = new Product(42, "fake product", 12); + + // Act + Basket basket = await function.Handle(product, new TestLambdaContext()); - // THEN + // Assert store.Invocations.Count.Should().Be(0); basket.Products.Count.Should().Be(1); function.HandlerExecuted.Should().BeTrue(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs similarity index 99% rename from libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTest.cs rename to libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs index 7236610f..d3f9dfbe 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs @@ -21,7 +21,7 @@ //Source: https://github.dev/microsoft/botbuilder-dotnet/blob/main/tests/AdaptiveExpressions.Tests/LRUCacheTest.cs namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; -public class LRUCacheTest +public class LRUCacheTests { [Fact] public void TestBasic() diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs index 5b156d87..d05c11ad 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs @@ -19,15 +19,10 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Model; public class Product { - public long Id { get; set; } - public string Name { get; set; } - public double Price { get; set; } - - public Product() - { - - } - + public long Id { get; } + public string Name { get; } + public double Price { get; } + public Product(long id, string name, double price) { Id = id; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index fefec3e7..398172eb 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -70,8 +70,9 @@ public override Task DeleteRecord(string idempotencyKey) } [Fact] - public async Task SaveInProgress_DefaultConfig() + public async Task SaveInProgress_WhenDefaultConfig_ShouldSaveRecordInStore() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -79,8 +80,10 @@ public async Task SaveInProgress_DefaultConfig() DateTimeOffset now = DateTimeOffset.UtcNow; + // Act await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + // Assert var dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); @@ -90,11 +93,10 @@ public async Task SaveInProgress_DefaultConfig() persistenceStore.Status.Should().Be(1); } - - [Fact] - public async Task SaveInProgress_jmespath() + public async Task SaveInProgress_WhenKeyJmesPathIsSet_ShouldSaveRecordInStore_WithIdempotencyKeyEqualsKeyJmesPath() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -103,8 +105,11 @@ public async Task SaveInProgress_jmespath() .Build(), "myfunc"); DateTimeOffset now = DateTimeOffset.UtcNow; + + // Act await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + // Assert var dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); @@ -116,8 +121,9 @@ public async Task SaveInProgress_jmespath() [Fact] - public async Task SaveInProgress_JMESPath_NotFound_ShouldThrowException() + public async Task SaveInProgress_WhenJMESPath_NotFound_ShouldThrowException() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -127,7 +133,10 @@ public async Task SaveInProgress_JMESPath_NotFound_ShouldThrowException() .Build(), ""); DateTimeOffset now = DateTimeOffset.UtcNow; + // Act Func act = async () => await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + + // Assert await act.Should() .ThrowAsync() .WithMessage("No data found to create a hashed idempotency key"); @@ -136,8 +145,9 @@ await act.Should() } [Fact] - public async Task SaveInProgress_JMESpath_NotFound_ShouldNotThrowException() + public async Task SaveInProgress_WhenJMESpath_NotFound_ShouldNotThrowException() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -146,16 +156,20 @@ public async Task SaveInProgress_JMESpath_NotFound_ShouldNotThrowException() .Build(), ""); DateTimeOffset now = DateTimeOffset.UtcNow; + + // Act await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + // Assert DataRecord dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); persistenceStore.Status.Should().Be(1); } [Fact] - public async Task SaveInProgress_WithLocalCache_NotExpired_ShouldThrowException() + public async Task SaveInProgress_WhenLocalCacheIsSet_AndNotExpired_ShouldThrowException() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -174,8 +188,10 @@ public async Task SaveInProgress_WithLocalCache_NotExpired_ShouldThrowException( null, null) ); + // Act Func act = () => persistenceStore.SaveInProgress(JToken.FromObject(request), now); + // Assert await act.Should() .ThrowAsync(); @@ -183,8 +199,9 @@ await act.Should() } [Fact] - public async Task SaveInProgress_WithLocalCache_Expired_ShouldRemoveFromCache() + public async Task SaveInProgress_WhenLocalCacheIsSetButExpired_ShouldRemoveFromCache() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -204,8 +221,10 @@ public async Task SaveInProgress_WithLocalCache_Expired_ShouldRemoveFromCache() null, null) ); + // Act await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + // Assert DataRecord dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); cache.Count.Should().Be(0); @@ -215,8 +234,9 @@ public async Task SaveInProgress_WithLocalCache_Expired_ShouldRemoveFromCache() ////// Save Success [Fact] - public async Task SaveSuccess_ShouldUpdateRecord() + public async Task SaveSuccess_WhenDefaultConfig_ShouldUpdateRecord() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); @@ -225,8 +245,11 @@ public async Task SaveSuccess_ShouldUpdateRecord() Product product = new Product(34543, "product", 42); DateTimeOffset now = DateTimeOffset.UtcNow; + + // Act await persistenceStore.SaveSuccess(JToken.FromObject(request), product, now); + // Assert DataRecord dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); @@ -238,8 +261,9 @@ public async Task SaveSuccess_ShouldUpdateRecord() } [Fact] - public async Task SaveSuccess_WithCacheEnabled_ShouldSaveInCache() + public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); @@ -250,8 +274,10 @@ public async Task SaveSuccess_WithCacheEnabled_ShouldSaveInCache() Product product = new Product(34543, "product", 42); DateTimeOffset now = DateTimeOffset.UtcNow; + // Act await persistenceStore.SaveSuccess(JToken.FromObject(request), product, now); + // Assert persistenceStore.Status.Should().Be(2); cache.Count.Should().Be(1); @@ -267,8 +293,9 @@ public async Task SaveSuccess_WithCacheEnabled_ShouldSaveInCache() /// Get Record [Fact] - public async Task GetRecord_ShouldReturnRecordFromPersistence() + public async Task GetRecord_WhenRecordIsInStore_ShouldReturnRecordFromPersistence() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -276,7 +303,11 @@ public async Task GetRecord_ShouldReturnRecordFromPersistence() persistenceStore.Configure(IdempotencyConfig.Builder().Build(), "myfunc", cache); DateTimeOffset now = DateTimeOffset.UtcNow; + + // Act DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + + // Assert record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); record.ResponseData.Should().Be("Response"); @@ -284,9 +315,9 @@ public async Task GetRecord_ShouldReturnRecordFromPersistence() } [Fact] - - public async Task GetRecord_CacheEnabledNotExpired_ShouldReturnRecordFromCache() + public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCache() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); @@ -303,7 +334,10 @@ public async Task GetRecord_CacheEnabledNotExpired_ShouldReturnRecordFromCache() null); cache.Set("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", dr); + // Act DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + + // Assert record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); record.ResponseData.Should().Be("result of the function"); @@ -311,8 +345,9 @@ public async Task GetRecord_CacheEnabledNotExpired_ShouldReturnRecordFromCache() } [Fact] - public async Task GetRecord_CacheEnabledExpired_ShouldReturnRecordFromPersistence() + public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRecordFromPersistence() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); @@ -328,7 +363,10 @@ public async Task GetRecord_CacheEnabledExpired_ShouldReturnRecordFromPersistenc null); cache.Set("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", dr); + // Act DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + + // Assert record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); record.ResponseData.Should().Be("Response"); @@ -337,8 +375,9 @@ public async Task GetRecord_CacheEnabledExpired_ShouldReturnRecordFromPersistenc } [Fact] - public async Task GetRecord_InvalidPayload_ShouldThrowValidationException() + public async Task GetRecord_WhenInvalidPayload_ShouldThrowValidationException() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); @@ -350,26 +389,35 @@ public async Task GetRecord_InvalidPayload_ShouldThrowValidationException() var validationHash = "different hash"; // "Lambda rocks" ==> 70c24d88041893f7fbab4105b76fd9e1 DateTimeOffset now = DateTimeOffset.UtcNow; + + // Act Func act = () => persistenceStore.GetRecord(JToken.FromObject(request), now); + + // Assert await act.Should().ThrowAsync(); } // Delete Record [Fact] - public async Task DeleteRecord_ShouldDeleteRecordFromPersistence() + public async Task DeleteRecord_WhenRecordExist_ShouldDeleteRecordFromPersistence() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + // Act await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); + + // Assert persistenceStore.Status.Should().Be(3); } [Fact] - public async Task DeleteRecord_CacheEnabled_ShouldDeleteRecordFromCache() + public async Task DeleteRecord_WhenLocalCacheEnabled_ShouldDeleteRecordFromCache() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); @@ -382,39 +430,57 @@ public async Task DeleteRecord_CacheEnabled_ShouldDeleteRecordFromCache() 123, null, null)); + // Act await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); + + // Assert persistenceStore.Status.Should().Be(3); cache.Count.Should().Be(0); } [Fact] - public void GenerateHashString_ShouldGenerateMd5ofString() + public void GenerateHash_WhenInputIsString_ShouldGenerateMd5ofString() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); string expectedHash = "70c24d88041893f7fbab4105b76fd9e1"; // MD5(Lambda rocks) + + // Act string generatedHash = persistenceStore.GenerateHash(new JValue("Lambda rocks")); + + // Assert generatedHash.Should().Be(expectedHash); } [Fact] - public void GenerateHashObject_ShouldGenerateMd5ofJsonObject() + public void GenerateHash_WhenInputIsObject_ShouldGenerateMd5ofJsonObject() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); Product product = new Product(42, "Product", 12); string expectedHash = "87dd2e12074c65c9bac728795a6ebb45"; // MD5({"Id":42,"Name":"Product","Price":12.0}) + + // Act string generatedHash = persistenceStore.GenerateHash(JToken.FromObject(product)); + + // Assert generatedHash.Should().Be(expectedHash); } [Fact] - public void GenerateHashDouble_ShouldGenerateMd5ofDouble() + public void GenerateHash_WhenInputIsDouble_ShouldGenerateMd5ofDouble() { + // Arrange var persistenceStore = new InMemoryPersistenceStore(); persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); string expectedHash = "bb84c94278119c8838649706df4db42b"; // MD5(256.42) + + // Act var generatedHash = persistenceStore.GenerateHash(new JValue(256.42)); + + // Assert generatedHash.Should().Be(expectedHash); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs index c4af3232..3f68de75 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DataRecordTests.cs @@ -23,49 +23,73 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; public class DataRecordTests { [Fact] - public void IsExpired_ShouldReturnTrue_WhenCurrentTimeIsGreaterThanExpiryTimestamp() + public void IsExpired_WhenCurrentTimeIsGreaterThanExpiryTimestamp_ShouldReturnTrue() { + // Arrange var now = DateTimeOffset.UtcNow; var dataRecord = new DataRecord( "123", DataRecord.DataRecordStatus.INPROGRESS, now.AddSeconds(-1).ToUnixTimeSeconds(), "abc","123"); - dataRecord.IsExpired(now).Should().BeTrue(); + + // Act + var result =dataRecord.IsExpired(now); + + // Assert + result.Should().BeTrue(); } [Fact] - public void IsExpired_ShouldReturnFalse_WhenCurrentTimeIsLessThanExpiryTimestamp() + public void IsExpired_WhenCurrentTimeIsLessThanExpiryTimestamp_ShouldReturnFalse() { + // Arrange var now = DateTimeOffset.UtcNow; var dataRecord = new DataRecord( "123", DataRecord.DataRecordStatus.INPROGRESS, now.AddSeconds(10).ToUnixTimeSeconds(), "abc","123"); - dataRecord.IsExpired(now).Should().BeFalse(); + + // Act + var result =dataRecord.IsExpired(now); + + // Assert + result.Should().BeFalse(); } [Fact] - public void Status_ShouldBeExpired_WhenCurrentTimeIsGreaterThanExpiryTimestamp() + public void Status_WhenCurrentTimeIsGreaterThanExpiryTimestamp_ShouldBeExpired() { + // Arrange var now = DateTimeOffset.UtcNow; var dataRecord = new DataRecord( "123", DataRecord.DataRecordStatus.INPROGRESS, now.AddSeconds(-10).ToUnixTimeSeconds(), "abc","123"); - dataRecord.Status.Should().Be(DataRecord.DataRecordStatus.EXPIRED); + + // Act + var status =dataRecord.Status; + + // Assert + status.Should().Be(DataRecord.DataRecordStatus.EXPIRED); } [Fact] - public void Status_ShouldBeRecordStatus_WhenCurrentTimeDidnotExpire() + public void Status_WhenCurrentTimeDidNotExpire_ShouldBeRecordStatus() { + // Arrange var now = DateTimeOffset.UtcNow; var dataRecord = new DataRecord( "123", DataRecord.DataRecordStatus.INPROGRESS, now.AddSeconds(10).ToUnixTimeSeconds(), "abc","123"); - dataRecord.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + + // Act + var status =dataRecord.Status; + + // Assert + status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index 33aa8ee9..a5fa642f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -42,15 +42,18 @@ public override async Task InitializeAsync() } //putRecord [Fact] - public async Task PutRecord_ShouldCreateRecordInDynamoDB() + public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB() { + // Arrange DateTimeOffset now = DateTimeOffset.UtcNow; long expiry = now.AddSeconds(3600).ToUnixTimeSeconds(); + var key = CreateKey("key"); + + // Act await _dynamoDbPersistenceStore .PutRecord(new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, null, null), now); - var key = CreateKey("key"); - + // Assert var getItemResponse = await client.GetItemAsync(new GetItemRequest { @@ -65,11 +68,12 @@ await client.GetItemAsync(new GetItemRequest } [Fact] - public async Task PutRecord_ShouldThrowIdempotencyItemAlreadyExistsException_IfRecordAlreadyExist() + public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlreadyExistsException() { + // Arrange var key = CreateKey("key"); - // GIVEN: Insert a fake item with same id + // Insert a fake item with same id Dictionary item = new(key); DateTimeOffset now = DateTimeOffset.UtcNow; long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); @@ -81,9 +85,9 @@ await client.PutItemAsync(new PutItemRequest TableName = TABLE_NAME, Item = item }); - - // WHEN: call putRecord long expiry2 = now.AddSeconds(3600).ToUnixTimeSeconds(); + + // Act Func act = () => _dynamoDbPersistenceStore.PutRecord( new DataRecord("key", DataRecord.DataRecordStatus.INPROGRESS, @@ -91,9 +95,11 @@ await client.PutItemAsync(new PutItemRequest null, null ), now); + + // Assert await act.Should().ThrowAsync(); - // THEN: item was not updated, retrieve the initial one + // item was not updated, retrieve the initial one Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest { TableName = TABLE_NAME, @@ -107,10 +113,10 @@ await client.PutItemAsync(new PutItemRequest //getRecord [Fact] - public async Task GetRecord_ShouldReturnExistingRecord() + public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecord() { - var key = new DictionaryEntry(); - // GIVEN: Insert a fake item with same id + // Arrange + // Insert a fake item with same id Dictionary item = new() { {"id", new AttributeValue("key")} //key @@ -129,10 +135,10 @@ public async Task GetRecord_ShouldReturnExistingRecord() Item = item }); - // WHEN + // Act DataRecord record = await _dynamoDbPersistenceStore.GetRecord("key"); - // THEN + // Assert record.IdempotencyKey.Should().Be("key"); record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); record.ResponseData.Should().Be("Fake Data"); @@ -140,17 +146,20 @@ public async Task GetRecord_ShouldReturnExistingRecord() } [Fact] - public async Task GetRecord_ShouldThrowException_WhenRecordIsAbsent() + public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() { + // Act Func act = () => _dynamoDbPersistenceStore.GetRecord("key"); + + // Assert await act.Should().ThrowAsync(); } //updateRecord [Fact] - public async Task UpdateRecord_ShouldUpdateRecord() + public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() { - // GIVEN: Insert a fake item with same id + // Arrange: Insert a fake item with same id var key = CreateKey("key"); Dictionary item = new(key); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -169,12 +178,12 @@ await client.PutItemAsync(new PutItemRequest _dynamoDbPersistenceStore.Configure(IdempotencyConfig.Builder().WithPayloadValidationJmesPath("path").Build(), null); - // WHEN + // Act expiry = now.AddSeconds(3600).ToUnixTimeMilliseconds(); DataRecord record = new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, "Fake result", "hash"); await _dynamoDbPersistenceStore.UpdateRecord(record); - // THEN + // Assert Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest { TableName = TABLE_NAME, @@ -189,9 +198,9 @@ await client.PutItemAsync(new PutItemRequest //deleteRecord [Fact] - public async Task DeleteRecord_ShouldDeleteRecord() + public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() { - // GIVEN: Insert a fake item with same id + // Arrange: Insert a fake item with same id var key = CreateKey("key"); Dictionary item = new(key); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -209,10 +218,10 @@ await client.PutItemAsync(new PutItemRequest }); scanResponse.Items.Count.Should().Be(1); - // WHEN + // Act await _dynamoDbPersistenceStore.DeleteRecord("key"); - // THEN + // Assert scanResponse = await client.ScanAsync(new ScanRequest { TableName = TABLE_NAME @@ -322,14 +331,19 @@ await client.DeleteTableAsync(new DeleteTableRequest } [Fact] - public async Task IdempotencyDisabled_NoClientShouldBeCreated() + public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() { try { + // Arrange Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); - + DynamoDBPersistenceStore store = DynamoDBPersistenceStore.Builder().WithTableName(TABLE_NAME).Build(); + + // Act Func act = () => store.GetRecord("fake"); + + // Assert await act.Should().ThrowAsync(); } finally From 036bb0ef7395311fd01f043d6c6a2f02befa6c25 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Tue, 20 Sep 2022 16:40:54 +0400 Subject: [PATCH 05/32] Fix HashFunction reference documentation --- docs/core/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/idempotency.md b/docs/core/idempotency.md index 2c9c095c..66a09666 100644 --- a/docs/core/idempotency.md +++ b/docs/core/idempotency.md @@ -223,7 +223,7 @@ These are the available options for further configuration: | **ThrowOnNoIdempotencyKey** | `False` | Throw exception if no idempotency key was found in the request | | **ExpirationInSeconds** | 3600 | The number of seconds to wait before a record is expired | | **UseLocalCache** | `false` | Whether to locally cache idempotency results (LRU cache) | -| **HashFunction** | `MD5` | Algorithm to use for calculating hashes, as supported by `java.security.MessageDigest` (eg. SHA-1, SHA-256, ...) | +| **HashFunction** | `MD5` | Algorithm to use for calculating hashes, as supported by `System.Security.Cryptography.HashAlgorithm` (eg. SHA1, SHA-256, ...) | These features are detailed below. From 73af08de12ed5a6ffc58982cef11e4f246d0c1cd Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Wed, 5 Oct 2022 13:45:22 +0400 Subject: [PATCH 06/32] Use idiomatic C# and terser configurations for Idempotency --- docs/core/idempotency.md | 204 ++++++------- .../Idempotency.cs | 103 ++++--- ...potencyConfig.cs => IdempotencyOptions.cs} | 25 +- .../Internal/IdempotencyHandler.cs | 6 +- .../Persistence/BasePersistenceStore.cs | 36 +-- .../Persistence/DynamoDBPersistenceStore.cs | 274 +++++++++--------- .../Handlers/IdempotencyFunction.cs | 30 +- .../Internal/IdempotentAspectTests.cs | 83 +++--- .../Persistence/BasePersistenceStoreTests.cs | 34 +-- .../DynamoDBPersistenceStoreTests.cs | 13 +- 10 files changed, 391 insertions(+), 417 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Idempotency/{IdempotencyConfig.cs => IdempotencyOptions.cs} (93%) diff --git a/docs/core/idempotency.md b/docs/core/idempotency.md index 66a09666..591f4ded 100644 --- a/docs/core/idempotency.md +++ b/docs/core/idempotency.md @@ -3,16 +3,13 @@ title: Idempotency description: Utility --- -The idempotency utility provides a simple solution to convert your Lambda functions into idempotent operations which -are safe to retry. +The idempotency utility provides a simple solution to convert your Lambda functions into idempotent operations which are safe to retry. ## Terminology -The property of idempotency means that an operation does not cause additional side effects if it is called more than -once with the same input parameters. +The property of idempotency means that an operation does not cause additional side effects if it is called more than once with the same input parameters. -**Idempotent operations will return the same result when they are called multiple -times with the same parameters**. This makes idempotent operations safe to retry. [Read more](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/) about idempotency. +**Idempotent operations will return the same result when they are called multiple times with the same parameters**. This makes idempotent operations safe to retry. [Read more](https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/) about idempotency. **Idempotency key** is a hash representation of either the entire event or a specific configured subset of the event, and invocation results are **JSON serialized** and stored in your persistence storage layer. @@ -20,7 +17,7 @@ times with the same parameters**. This makes idempotent operations safe to retry * Prevent Lambda handler function from executing more than once on the same event payload during a time window * Ensure Lambda handler returns the same result when called with the same payload -* Select a subset of the event as the idempotency key using JMESPath expressions +* 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 ## Getting started @@ -29,13 +26,13 @@ times with the same parameters**. This makes idempotent operations safe to retry You should install with NuGet: ``` -Install-Package Amazon.Lambda.PowerTools.Idempotency +Install-Package AWS.Lambda.Powertools.Idempotency ``` Or via the .NET Core command line interface: ``` -dotnet add package Amazon.Lambda.PowerTools.Idempotency +dotnet add package AWS.Lambda.Powertools.Idempotency ``` ### Required resources @@ -56,34 +53,36 @@ If you're not [changing the default configuration for the DynamoDB persistence l !!! Tip "Tip: You can share a single state table for all functions" You can reuse the same DynamoDB table to store idempotency state. We add your function name in addition to the idempotency key as a hash key. -```yaml hl_lines="5-13 21-23 26" title="AWS Serverless Application Model (SAM) example" -Resources: - IdempotencyTable: - Type: AWS::DynamoDB::Table - Properties: - AttributeDefinitions: - - AttributeName: id - AttributeType: S - KeySchema: - - AttributeName: id - KeyType: HASH - TimeToLiveSpecification: - AttributeName: expiration - Enabled: true - BillingMode: PAY_PER_REQUEST - - IdempotencyFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: Function - Handler: HelloWorld::HelloWorld.Function::FunctionHandler - Policies: - - DynamoDBCrudPolicy: - TableName: !Ref IdempotencyTable - Environment: - Variables: - IDEMPOTENCY_TABLE: !Ref IdempotencyTable -``` +=== "template.yml" + + ```yaml hl_lines="5-13 21-23 26" title="AWS Serverless Application Model (SAM) example" + Resources: + IdempotencyTable: + Type: AWS::DynamoDB::Table + Properties: + AttributeDefinitions: + - AttributeName: id + AttributeType: S + KeySchema: + - AttributeName: id + KeyType: HASH + TimeToLiveSpecification: + AttributeName: expiration + Enabled: true + BillingMode: PAY_PER_REQUEST + + IdempotencyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: Function + Handler: HelloWorld::HelloWorld.Function::FunctionHandler + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref IdempotencyTable + Environment: + Variables: + IDEMPOTENCY_TABLE: !Ref IdempotencyTable + ``` !!! warning "Warning: Large responses with DynamoDB persistence layer" When using this utility with DynamoDB, your function's responses must be [smaller than 400KB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-items). @@ -97,25 +96,17 @@ Resources: ### Idempotent attribute -You can quickly start by initializing the `DynamoDBPersistenceStore` and using it with the `Idempotent` attribute on your Lambda function. +You can quickly start by configuring `Idempotency` and using it with the `Idempotent` attribute on your Lambda function. !!! warning "Important" - Initialization and configuration of the `DynamoDBPersistenceStore` must be performed outside the handler, preferably in the constructor. + Initialization and configuration of the `Idempotency` must be performed outside the handler, preferably in the constructor. ```csharp public class Function { public Function() { - Idempotency.Config() - .WithConfig( - IdempotencyConfig.Builder() - .Build()) - .WithPersistenceStore( - DynamoDBPersistenceStore.Builder() - .WithTableName("Lambda_Is_Cool") - .Build() - ).Configure(); + Idempotency.Configure(builder => builder.UseDynamoDb("idempotency_table")); } [Idempotent] @@ -142,18 +133,14 @@ Imagine the function executes successfully, but the client never receives the re !!! warning "Warning: Idempotency for JSON payloads" The payload extracted by the `EventKeyJmesPath` is treated as a string by default, so will be sensitive to differences in whitespace even when the JSON payload itself is identical. - To alter this behaviour, you can use the [JMESPath built-in function](utilities.md#powertools_json-function) `powertools_json()` to treat the payload as a JSON object rather than a string. + To alter this behaviour, you can use the JMESPath built-in function `powertools_json()` to treat the payload as a JSON object rather than a string. ```csharp - Idempotency.Config() - .WithConfig( - IdempotencyConfig.Builder() - .Build()) - .WithPersistenceStore( - DynamoDBPersistenceStore.Builder() - .WithTableName("Lambda_Is_Cool") - .Build() - ).Configure(); + Idempotency.Configure(builder => + builder + .WithOptions(optionsBuilder => + optionsBuilder.WithEventKeyJmesPath("powertools_json(Body).address")) + .UseDynamoDb("idempotency_table")); ``` ### Handling exceptions @@ -174,14 +161,14 @@ This persistence store is built-in, and you can either use an existing DynamoDB Use the builder to customize the table structure: ```csharp title="Customizing DynamoDBPersistenceStore to suit your table structure" -DynamoDBPersistenceStore.Builder() - .WithTableName(System.getenv("TABLE_NAME")) - .WithKeyAttr("idempotency_key") - .WithExpiryAttr("expires_at") - .WithStatusAttr("current_status") - .WithDataAttr("result_data") - .WithValidationAttr("validation_key") - .Build() +new DynamoDBPersistenceStoreBuilder() + .WithTableName(System.getenv("TABLE_NAME")) + .WithKeyAttr("idempotency_key") + .WithExpiryAttr("expires_at") + .WithStatusAttr("current_status") + .WithDataAttr("result_data") + .WithValidationAttr("validation_key") + .Build() ``` When using DynamoDB as a persistence layer, you can alter the attribute names by passing these parameters when initializing the persistence layer: @@ -204,26 +191,26 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by Idempotency behavior can be further configured with **`IdempotencyConfig`** using a builder: ```csharp -IdempotencyConfig.Builder() - .WithEventKeyJmesPath("id") - .WithPayloadValidationJmesPath("paymentId") - .WithThrowOnNoIdempotencyKey(true) - .WithExpiration(TimeSpan.FromMinutes(1)) - .WithUseLocalCache(true) - .WithHashFunction("MD5") - .Build(); +new IdempotencyConfigBuilder() + .WithEventKeyJmesPath("id") + .WithPayloadValidationJmesPath("paymentId") + .WithThrowOnNoIdempotencyKey(true) + .WithExpiration(TimeSpan.FromMinutes(1)) + .WithUseLocalCache(true) + .WithHashFunction("MD5") + .Build(); ``` These are the available options for further configuration: | Parameter | Default | Description | |---------------------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------| -| **EventKeyJMESPath** | `""` | JMESPath expression to extract the idempotency key from the event record. See available [built-in functions](serialization) | +| **EventKeyJMESPath** | `""` | JMESPath expression to extract the idempotency key from the event record. | | **PayloadValidationJMESPath** | `""` | JMESPath expression to validate whether certain parameters have changed in the event | | **ThrowOnNoIdempotencyKey** | `False` | Throw exception if no idempotency key was found in the request | | **ExpirationInSeconds** | 3600 | The number of seconds to wait before a record is expired | | **UseLocalCache** | `false` | Whether to locally cache idempotency results (LRU cache) | -| **HashFunction** | `MD5` | Algorithm to use for calculating hashes, as supported by `System.Security.Cryptography.HashAlgorithm` (eg. SHA1, SHA-256, ...) | +| **HashFunction** | `MD5` | Algorithm to use for calculating hashes, as supported by `System.Security.Cryptography.HashAlgorithm` (eg. SHA1, SHA-256, ...) | These features are detailed below. @@ -245,7 +232,7 @@ This is a locking mechanism for correctness. Since we don't know the result from You can enable it as seen before with: ```csharp title="Enable local cache" - IdempotencyConfig.Builder() + new IdempotencyConfigBuilder() .WithUseLocalCache(true) .Build() ``` @@ -264,7 +251,7 @@ In most cases, it is not desirable to store the idempotency records forever. Rat You can change this window with the **`ExpirationInSeconds`** parameter: ```csharp title="Customizing expiration time" -IdempotencyConfig.Builder() +new IdempotencyConfigBuilder() .WithExpiration(TimeSpan.FromMinutes(5)) .Build() ``` @@ -283,18 +270,16 @@ By default, we will return the same result as it returned before, however in thi With **`PayloadValidationJMESPath`**, you can provide an additional JMESPath expression to specify which part of the event body should be validated against previous idempotent invocations -=== "App.java" +=== "Function.cs" ```csharp - Idempotency.Config() - .WithPersistenceStore(DynamoDBPersistenceStore.Builder() - .WithTableName("TABLE_NAME") - .Build()) - .WithConfig(IdempotencyConfig.Builder() - .WithEventKeyJmesPath("[userDetail, productId]") - .WithPayloadValidationJmesPath("amount") - .Build()) - .Configure(); + Idempotency.Configure(builder => + builder + .WithOptions(optionsBuilder => + optionsBuilder + .WithEventKeyJmesPath("[userDetail, productId]") + .WithPayloadValidationJmesPath("amount")) + .UseDynamoDb("TABLE_NAME")); ``` === "Example Event 1" @@ -345,16 +330,14 @@ This means that we will throw **`IdempotencyKeyException`** if the evaluation of ```csharp public App() { - Idempotency.Config() - .WithPersistenceStore(DynamoDBPersistenceStore.Builder() - .WithTableName("TABLE_NAME") - .Build()) - .WithConfig(IdempotencyConfig.Builder() - // Requires "user"."uid" and "orderId" to be present - .WithEventKeyJmesPath("[user.uid, orderId]") - .WithThrowOnNoIdempotencyKey(true) - .Build()) - .Configure(); + Idempotency.Configure(builder => + builder + .WithOptions(optionsBuilder => + optionsBuilder + // Requires "user"."uid" and "orderId" to be present + .WithEventKeyJmesPath("[user.uid, orderId]") + .WithThrowOnNoIdempotencyKey(true)) + .UseDynamoDb("TABLE_NAME")); } [Idempotent] @@ -401,12 +384,12 @@ When creating the `DynamoDBPersistenceStore`, you can set a custom [`AmazonDynam { AmazonDynamoDBClient customClient = new AmazonDynamoDBClient(RegionEndpoint.APSouth1); - Idempotency.Config().WithPersistenceStore( - DynamoDBPersistenceStore.Builder() - .WithTableName("TABLE_NAME") - .WithDynamoDBClient(customClient) - .Build() - ).Configure(); + Idempotency.Configure(builder => + builder.UseDynamoDb(storeBuilder => + storeBuilder. + WithTableName("TABLE_NAME") + .WithDynamoDBClient(customClient) + )); } ``` @@ -419,13 +402,12 @@ With this setting, we will save the idempotency key in the sort key instead of t You can optionally set a static value for the partition key using the `StaticPkValue` parameter. ```csharp title="Reusing a DynamoDB table that uses a composite primary key" -Idempotency.Config() - .WithPersistenceStore( - DynamoDBPersistenceStore.Builder() - .WithTableName(("TABLE_NAME")) +Idempotency.Configure(builder => + builder.UseDynamoDb(storeBuilder => + storeBuilder. + WithTableName("TABLE_NAME") .WithSortKeyAttr("sort_key") - .Build()) - .Configure(); + )); ``` Data would then be stored in DynamoDB like this: @@ -436,8 +418,6 @@ Data would then be stored in DynamoDB like this: | idempotency#MyLambdaFunction | 2b2cdb5f86361e97b4383087c1ffdf27 | 1636549571 | COMPLETED | {"id": 527212, "message": "success"} | | idempotency#MyLambdaFunction | f091d2527ad1c78f05d54cc3f363be80 | 1636549585 | IN_PROGRESS | | -## Compatibility with other utilities - ## Testing your code The idempotency utility provides several routes to test your code. diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 94c20168..8a2ee69f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -23,39 +23,27 @@ namespace AWS.Lambda.Powertools.Idempotency; /// The persistence layer to use for persisting the request and response of the function (mandatory). /// The general configuration for idempotency (optional, see {@link IdempotencyConfig.Builder} methods to see defaults values. /// Use it before the function handler get called. -/// Example: Idempotency.Config().WithPersistenceStore(...).Configure(); +/// Example: Idempotency.Configure(builder => builder.WithPersistenceStore(...)); /// public class Idempotency { - private IdempotencyConfig _idempotencyConfig; - private BasePersistenceStore _persistenceStore; + public IdempotencyOptions IdempotencyOptions { get; private set; } + public BasePersistenceStore PersistenceStore { get; private set; } private Idempotency() { } + + - public IdempotencyConfig GetIdempotencyConfig() + private void SetConfig(IdempotencyOptions options) { - return _idempotencyConfig; - } - - public BasePersistenceStore GetPersistenceStore() - { - if (_persistenceStore == null) - { - throw new NullReferenceException("Persistence Store is null, did you call 'Configure()'?"); - } - return _persistenceStore; - } - - private void SetConfig(IdempotencyConfig config) - { - _idempotencyConfig = config; + IdempotencyOptions = options; } private void SetPersistenceStore(BasePersistenceStore persistenceStore) { - _persistenceStore = persistenceStore; + PersistenceStore = persistenceStore; } private static class Holder { @@ -65,49 +53,70 @@ private static class Holder { public static Idempotency Instance() => Holder.IdempotencyInstance; /// - /// Acts like a builder that can be used to configure Idempotency + /// Use this method to configure persistence layer (mandatory) and idempotency options (optional) /// /// - public static IdempotencyBuilder Config() + public static void Configure(Action configurationAction) { - return new IdempotencyBuilder(); + var builder = new IdempotencyBuilder(); + configurationAction(builder); + if (builder.Store == null) + { + throw new NullReferenceException("Persistence Layer is null, configure one with 'WithPersistenceStore()'"); + } + if (builder.Options != null) + { + Instance().SetConfig(builder.Options); + } + else + { + Instance().SetConfig(new IdempotencyConfigBuilder().Build()); + } + Instance().SetPersistenceStore(builder.Store); } public class IdempotencyBuilder { - private IdempotencyConfig _config; + private IdempotencyOptions _options; private BasePersistenceStore _store; - - /// - /// Use this method after configuring persistence layer (mandatory) and idem potency configuration (optional) - /// - /// - public void Configure() + + public IdempotencyOptions Options => _options; + public BasePersistenceStore Store => _store; + + public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceStore) { - if (_store == null) - { - throw new NullReferenceException("Persistence Layer is null, configure one with 'WithPersistenceStore()'"); - } - if (_config == null) - { - _config = IdempotencyConfig.Builder().Build(); - } - Idempotency.Instance().SetConfig(_config); - Idempotency.Instance().SetPersistenceStore(_store); + _store = persistenceStore; + return this; + } + public IdempotencyBuilder UseDynamoDb(Action builderAction) + { + DynamoDBPersistenceStoreBuilder builder = + new DynamoDBPersistenceStoreBuilder(); + builderAction(builder); + _store = builder.Build(); + return this; } - public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceStore) + public IdempotencyBuilder UseDynamoDb(string tableName) { - this._store = persistenceStore; + DynamoDBPersistenceStoreBuilder builder = + new DynamoDBPersistenceStoreBuilder(); + _store = builder.WithTableName(tableName).Build(); return this; } - public IdempotencyBuilder WithConfig(IdempotencyConfig config) + public IdempotencyBuilder WithOptions(Action builderAction) { - this._config = config; + IdempotencyConfigBuilder builder = new IdempotencyConfigBuilder(); + builderAction(builder); + _options = builder.Build(); + return this; + } + + public IdempotencyBuilder WithOptions(IdempotencyOptions options) + { + _options = options; return this; } } - - -} +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs similarity index 93% rename from libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs rename to libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs index 3bafecd2..79cc68e6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyConfig.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs @@ -21,7 +21,7 @@ namespace AWS.Lambda.Powertools.Idempotency; /// /// Configuration of the idempotency feature. Use the Builder to create an instance. /// -public class IdempotencyConfig +public class IdempotencyOptions { public string EventKeyJmesPath { get; } public string PayloadValidationJmesPath { get; } @@ -32,7 +32,7 @@ public class IdempotencyConfig public string HashFunction { get; } public ILog Log { get; } - private IdempotencyConfig( + internal IdempotencyOptions( string eventKeyJmesPath, string payloadValidationJmesPath, bool throwOnNoIdempotencyKey, @@ -51,13 +51,8 @@ private IdempotencyConfig( HashFunction = hashFunction; Log = log; } - - public static IdempotencyConfigBuilder Builder() - { - return new IdempotencyConfigBuilder(); - } - - public class IdempotencyConfigBuilder +} +public class IdempotencyConfigBuilder { private int _localCacheMaxItems = 256; private bool _useLocalCache = false; @@ -76,8 +71,8 @@ public class IdempotencyConfigBuilder /// Idempotency.Config().WithConfig(config).Configure(); /// /// an instance of IdempotencyConfig - public IdempotencyConfig Build() => - new IdempotencyConfig(_eventKeyJmesPath, + public IdempotencyOptions Build() => + new IdempotencyOptions(_eventKeyJmesPath, _payloadValidationJmesPath, _throwOnNoIdempotencyKey, _useLocalCache, @@ -158,15 +153,11 @@ public IdempotencyConfigBuilder WithHashFunction(string hashFunction) /// /// Logs to a custom logger. /// - /// /// The logger. - /// - /// The same builder - /// + /// the instance of the builder (to chain operations) public IdempotencyConfigBuilder LogTo(ILog log) { _log = log; return this; } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs index d03dfc23..ab100a9f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -42,9 +42,9 @@ public IdempotencyHandler( _target = target; _args = args; _data = payload; - _persistenceStore = Idempotency.Instance().GetPersistenceStore(); - _persistenceStore.Configure(Idempotency.Instance().GetIdempotencyConfig(), functionName); - _log = Idempotency.Instance().GetIdempotencyConfig().Log; + _persistenceStore = Idempotency.Instance().PersistenceStore; + _persistenceStore.Configure(Idempotency.Instance().IdempotencyOptions, functionName); + _log = Idempotency.Instance().IdempotencyOptions.Log; } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index f090d2ed..7b61e33c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -29,13 +29,13 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; public abstract class BasePersistenceStore : IPersistenceStore { - private IdempotencyConfig _idempotencyConfig; + private IdempotencyOptions _idempotencyOptions; private string? _functionName; protected bool PayloadValidationEnabled; private LRUCache _cache = null!; protected ILog Log; - public void Configure(IdempotencyConfig idempotencyConfig, string? functionName) + public void Configure(IdempotencyOptions idempotencyOptions, string? functionName) { string? funcEnv = Environment.GetEnvironmentVariable(Constants.LAMBDA_FUNCTION_NAME_ENV); _functionName = funcEnv != null ? funcEnv : "testFunction"; @@ -43,28 +43,28 @@ public void Configure(IdempotencyConfig idempotencyConfig, string? functionName) { _functionName += "." + functionName; } - _idempotencyConfig = idempotencyConfig; - Log = _idempotencyConfig.Log; + _idempotencyOptions = idempotencyOptions; + Log = _idempotencyOptions.Log; //TODO: optimize to not reconfigure - if (!string.IsNullOrWhiteSpace(_idempotencyConfig.PayloadValidationJmesPath)) + if (!string.IsNullOrWhiteSpace(_idempotencyOptions.PayloadValidationJmesPath)) { PayloadValidationEnabled = true; } - var useLocalCache = _idempotencyConfig.UseLocalCache; + var useLocalCache = _idempotencyOptions.UseLocalCache; if (useLocalCache) { - _cache = new (_idempotencyConfig.LocalCacheMaxItems); + _cache = new (_idempotencyOptions.LocalCacheMaxItems); } } /// /// For test purpose only (adding a cache to mock) /// - internal void Configure(IdempotencyConfig config, string functionName, LRUCache cache) + internal void Configure(IdempotencyOptions options, string functionName, LRUCache cache) { - Configure(config, functionName); + Configure(options, functionName); _cache = cache; } @@ -153,7 +153,7 @@ public virtual async Task GetRecord(JToken data, DateTimeOffset now) /// DataRecord to save in cache private void SaveToCache(DataRecord dataRecord) { - if (!_idempotencyConfig.UseLocalCache) + if (!_idempotencyOptions.UseLocalCache) return; if (dataRecord.Status == DataRecord.DataRecordStatus.INPROGRESS) return; @@ -182,7 +182,7 @@ private void ValidatePayload(JToken data, DataRecord dataRecord) private DataRecord? RetrieveFromCache(string idempotencyKey, DateTimeOffset now) { - if (!_idempotencyConfig.UseLocalCache) + if (!_idempotencyOptions.UseLocalCache) return null; if (_cache.TryGet(idempotencyKey, out DataRecord record) && record!=null) @@ -198,7 +198,7 @@ private void ValidatePayload(JToken data, DataRecord dataRecord) } private void DeleteFromCache(string idempotencyKey) { - if (!_idempotencyConfig.UseLocalCache) + if (!_idempotencyOptions.UseLocalCache) return; _cache.Delete(idempotencyKey); @@ -218,7 +218,7 @@ private string GetHashedPayload(JToken data) var jmes = new JmesPath(); jmes.FunctionRepository.Register(); - var result = jmes.Transform(data.ToString(), _idempotencyConfig.PayloadValidationJmesPath); + var result = jmes.Transform(data.ToString(), _idempotencyOptions.PayloadValidationJmesPath); var node = JToken.Parse(result); return GenerateHash(node); } @@ -232,7 +232,7 @@ private string GetHashedPayload(JToken data) /// unix timestamp of expiry date for idempotency record private long GetExpiryEpochSecond(DateTimeOffset now) { - return now.AddSeconds(_idempotencyConfig.ExpirationInSeconds).ToUnixTimeSeconds(); + return now.AddSeconds(_idempotencyOptions.ExpirationInSeconds).ToUnixTimeSeconds(); } /// @@ -244,7 +244,7 @@ private long GetExpiryEpochSecond(DateTimeOffset now) private string GetHashedIdempotencyKey(JToken data) { JToken node = data; - var eventKeyJmesPath = _idempotencyConfig.EventKeyJmesPath; + var eventKeyJmesPath = _idempotencyOptions.EventKeyJmesPath; if (eventKeyJmesPath != null) { var jmes = new JmesPath(); @@ -255,11 +255,11 @@ private string GetHashedIdempotencyKey(JToken data) if (IsMissingIdemPotencyKey(node)) { - if (_idempotencyConfig.ThrowOnNoIdempotencyKey) + if (_idempotencyOptions.ThrowOnNoIdempotencyKey) { throw new IdempotencyKeyException("No data found to create a hashed idempotency key"); } - Log.WriteWarning("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyConfig.EventKeyJmesPath); + Log.WriteWarning("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyOptions.EventKeyJmesPath); } string hash = GenerateHash(node); @@ -301,7 +301,7 @@ internal string GenerateHash(JToken data) else node = data; // anything else - using var hashAlgorithm = HashAlgorithm.Create(_idempotencyConfig.HashFunction); + using var hashAlgorithm = HashAlgorithm.Create(_idempotencyOptions.HashFunction); if (hashAlgorithm == null) { throw new ArgumentException("Invalid HashAlgorithm"); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index 7925f1e3..130357e2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -35,7 +35,7 @@ public class DynamoDBPersistenceStore : BasePersistenceStore private readonly string _validationAttr; private readonly AmazonDynamoDBClient _dynamoDbClient; - private DynamoDBPersistenceStore(string tableName, + internal DynamoDBPersistenceStore(string tableName, string keyAttr, string staticPkValue, string? sortKeyAttr, @@ -229,146 +229,146 @@ private Dictionary GetKey(string idempotencyKey) return key; } - public static DynamoDBPersistenceStoreBuilder Builder() => new DynamoDBPersistenceStoreBuilder(); +} - public class DynamoDBPersistenceStoreBuilder +public class DynamoDBPersistenceStoreBuilder +{ + private static readonly string? FuncEnv = Environment.GetEnvironmentVariable(Constants.LAMBDA_FUNCTION_NAME_ENV); + + private string _tableName = null!; + private string _keyAttr = "id"; + private string _staticPkValue = string.Format("idempotency#%s", FuncEnv ?? ""); + private string? _sortKeyAttr = null; + private string _expiryAttr = "expiration"; + private string _statusAttr = "status"; + private string _dataAttr = "data"; + private string _validationAttr = "validation"; + private AmazonDynamoDBClient _dynamoDbClient; + + /// + /// Initialize and return a new instance of {@link DynamoDBPersistenceStore}. + /// Example: + /// DynamoDBPersistenceStore.builder().withTableName("idempotency_store").build(); + /// + /// + /// + public DynamoDBPersistenceStore Build() { - private static readonly string? FuncEnv = Environment.GetEnvironmentVariable(Constants.LAMBDA_FUNCTION_NAME_ENV); - - private string _tableName = null!; - private string _keyAttr = "id"; - private string _staticPkValue = string.Format("idempotency#%s", FuncEnv ?? ""); - private string? _sortKeyAttr = null; - private string _expiryAttr = "expiration"; - private string _statusAttr = "status"; - private string _dataAttr = "data"; - private string _validationAttr = "validation"; - private AmazonDynamoDBClient _dynamoDbClient; - - /// - /// Initialize and return a new instance of {@link DynamoDBPersistenceStore}. - /// Example: - /// DynamoDBPersistenceStore.builder().withTableName("idempotency_store").build(); - /// - /// - /// - public DynamoDBPersistenceStore Build() - { - if (string.IsNullOrWhiteSpace(_tableName)) - { - throw new ArgumentNullException("Table name is not specified"); - } - return new DynamoDBPersistenceStore(_tableName, - _keyAttr, - _staticPkValue, - _sortKeyAttr, - _expiryAttr, - _statusAttr, - _dataAttr, - _validationAttr, - _dynamoDbClient); - } - - /// - /// Name of the table to use for storing execution records (mandatory) - /// - /// tableName Name of the DynamoDB table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithTableName(string tableName) + if (string.IsNullOrWhiteSpace(_tableName)) { - _tableName = tableName; - return this; - } - - /// - /// DynamoDB attribute name for partition key (optional), by default "id" - /// - /// keyAttr name of the key attribute in the table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithKeyAttr(string keyAttr) - { - _keyAttr = keyAttr; - return this; + throw new ArgumentNullException("Table name is not specified"); } - /// - /// DynamoDB attribute value for partition key (optional), by default "idempotency#[function-name]". - /// This will be used if the {@link #sortKeyAttr} is set. - /// - /// staticPkValue name of the partition key attribute in the table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithStaticPkValue(string staticPkValue) - { - _staticPkValue = staticPkValue; - return this; - } - - /// - /// DynamoDB attribute name for the sort key (optional) - /// - /// sortKeyAttr name of the sort key attribute in the table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithSortKeyAttr(string sortKeyAttr) - { - _sortKeyAttr = sortKeyAttr; - return this; - } - - /// - /// DynamoDB attribute name for expiry timestamp (optional), by default "expiration" - /// - /// expiryAttr name of the expiry attribute in the table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithExpiryAttr(string expiryAttr) - { - _expiryAttr = expiryAttr; - return this; - } - - /// - /// DynamoDB attribute name for status (optional), by default "status" - /// - /// statusAttr name of the status attribute in the table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithStatusAttr(string statusAttr) - { - _statusAttr = statusAttr; - return this; - } - - /// - /// DynamoDB attribute name for response data (optional), by default "data" - /// - /// dataAttr name of the data attribute in the table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithDataAttr(string dataAttr) - { - _dataAttr = dataAttr; - return this; - } - - /// - /// DynamoDB attribute name for validation (optional), by default "validation" - /// - /// validationAttr name of the validation attribute in the table - /// the builder instance (to chain operations) - public DynamoDBPersistenceStoreBuilder WithValidationAttr(string validationAttr) - { - _validationAttr = validationAttr; - return this; - } - - /// - /// Custom DynamoDbClient used to query DynamoDB (optional). - /// The default one uses UrlConnectionHttpClient as a http client and - /// - /// dynamoDbClient the DynamoDbClient instance to use - /// the builder instance (to chain operations) - // ReSharper disable once InconsistentNaming - public DynamoDBPersistenceStoreBuilder WithDynamoDBClient(AmazonDynamoDBClient dynamoDbClient) - { - _dynamoDbClient = dynamoDbClient; - return this; - } + return new DynamoDBPersistenceStore(_tableName, + _keyAttr, + _staticPkValue, + _sortKeyAttr, + _expiryAttr, + _statusAttr, + _dataAttr, + _validationAttr, + _dynamoDbClient); + } + + /// + /// Name of the table to use for storing execution records (mandatory) + /// + /// tableName Name of the DynamoDB table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithTableName(string tableName) + { + _tableName = tableName; + return this; + } + + /// + /// DynamoDB attribute name for partition key (optional), by default "id" + /// + /// keyAttr name of the key attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithKeyAttr(string keyAttr) + { + _keyAttr = keyAttr; + return this; + } + + /// + /// DynamoDB attribute value for partition key (optional), by default "idempotency#[function-name]". + /// This will be used if the {@link #sortKeyAttr} is set. + /// + /// staticPkValue name of the partition key attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithStaticPkValue(string staticPkValue) + { + _staticPkValue = staticPkValue; + return this; + } + + /// + /// DynamoDB attribute name for the sort key (optional) + /// + /// sortKeyAttr name of the sort key attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithSortKeyAttr(string sortKeyAttr) + { + _sortKeyAttr = sortKeyAttr; + return this; + } + + /// + /// DynamoDB attribute name for expiry timestamp (optional), by default "expiration" + /// + /// expiryAttr name of the expiry attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithExpiryAttr(string expiryAttr) + { + _expiryAttr = expiryAttr; + return this; + } + + /// + /// DynamoDB attribute name for status (optional), by default "status" + /// + /// statusAttr name of the status attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithStatusAttr(string statusAttr) + { + _statusAttr = statusAttr; + return this; + } + + /// + /// DynamoDB attribute name for response data (optional), by default "data" + /// + /// dataAttr name of the data attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithDataAttr(string dataAttr) + { + _dataAttr = dataAttr; + return this; + } + + /// + /// DynamoDB attribute name for validation (optional), by default "validation" + /// + /// validationAttr name of the validation attribute in the table + /// the builder instance (to chain operations) + public DynamoDBPersistenceStoreBuilder WithValidationAttr(string validationAttr) + { + _validationAttr = validationAttr; + return this; + } + + /// + /// Custom DynamoDbClient used to query DynamoDB (optional). + /// The default one uses UrlConnectionHttpClient as a http client and + /// + /// dynamoDbClient the DynamoDbClient instance to use + /// the builder instance (to chain operations) + // ReSharper disable once InconsistentNaming + public DynamoDBPersistenceStoreBuilder WithDynamoDBClient(AmazonDynamoDBClient dynamoDbClient) + { + _dynamoDbClient = dynamoDbClient; + return this; } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs index 8d32f87b..f296b5b8 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs @@ -17,10 +17,10 @@ using System.IO; using System.Net.Http; using System.Threading.Tasks; +using Amazon; using Amazon.DynamoDBv2; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; -using AWS.Lambda.Powertools.Idempotency.Persistence; using Newtonsoft.Json.Linq; namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; @@ -31,18 +31,18 @@ public class IdempotencyFunction public IdempotencyFunction(AmazonDynamoDBClient client) { - AWS.Lambda.Powertools.Idempotency.Idempotency.Config() - .WithConfig( - IdempotencyConfig.Builder() - .WithEventKeyJmesPath("powertools_json(Body).address") - .Build()) - .WithPersistenceStore( - DynamoDBPersistenceStore.Builder() - .WithTableName("idempotency_table") - .WithDynamoDBClient(client) - .Build() - ).Configure(); + Idempotency.Configure(builder => + builder + .WithOptions(optionsBuilder => + optionsBuilder.WithEventKeyJmesPath("powertools_json(Body).address")) + .UseDynamoDb(storeBuilder => + storeBuilder + .WithTableName("idempotency_table") + .WithDynamoDBClient(client) + )); + + } [Idempotent] @@ -66,7 +66,7 @@ private async Task InternalFunctionHandler(APIGatewayPr try { string address = JToken.Parse(apigProxyEvent.Body)["address"].Value(); - string pageContents = await getPageContents(address); + string pageContents = await GetPageContents(address); string output = $"{{ \"message\": \"hello world\", \"location\": \"{pageContents}\" }}"; return new APIGatewayProxyResponse @@ -77,7 +77,7 @@ private async Task InternalFunctionHandler(APIGatewayPr }; } - catch (IOException e) + catch (IOException) { return new APIGatewayProxyResponse { @@ -89,7 +89,7 @@ private async Task InternalFunctionHandler(APIGatewayPr } // we could actually also put the @Idempotent annotation here - private async Task getPageContents(string address) + private async Task GetPageContents(string address) { HttpClient client = new HttpClient(); using HttpResponseMessage response = await client.GetAsync(address); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index b0580c03..b6b7197c 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -34,12 +34,11 @@ public async Task Handle_WhenFirstCall_ShouldPutInStore() { //Arrange var store = new Mock(); - Idempotency.Config() - .WithPersistenceStore(store.Object) - .WithConfig(IdempotencyConfig.Builder() - .WithEventKeyJmesPath("Id") - .Build() - ).Configure(); + Idempotency.Configure(builder => + builder + .WithPersistenceStore(store.Object) + .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) + ); IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); Product product = new Product(42, "fake product", 12); @@ -65,15 +64,14 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() var store = new Mock(); store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) .Throws(); - + // GIVEN - Idempotency.Config() - .WithPersistenceStore(store.Object) - .WithConfig(IdempotencyConfig.Builder() - .WithEventKeyJmesPath("Id") - .Build() - ).Configure(); - + Idempotency.Configure(builder => + builder + .WithPersistenceStore(store.Object) + .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) + ); + Product product = new Product(42, "fake product", 12); Basket basket = new Basket(product); DataRecord record = new DataRecord( @@ -84,32 +82,31 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() null); store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); - + IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); - + // Act Basket resultBasket = await function.Handle(product, new TestLambdaContext()); - + // Assert resultBasket.Should().Be(basket); function.HandlerExecuted.Should().BeFalse(); } - + [Fact] public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempotencyAlreadyInProgressException() { // Arrange var store = new Mock(); - Idempotency.Config() - .WithPersistenceStore(store.Object) - .WithConfig(IdempotencyConfig.Builder() - .WithEventKeyJmesPath("Id") - .Build() - ).Configure(); + Idempotency.Configure(builder => + builder + .WithPersistenceStore(store.Object) + .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) + ); store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) .Throws(); - + Product product = new Product(42, "fake product", 12); Basket basket = new Basket(product); DataRecord record = new DataRecord( @@ -120,11 +117,11 @@ public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempoten null); store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); - + // Act IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); Func act = async () => await function.Handle(product, new TestLambdaContext()); - + // Assert await act.Should().ThrowAsync(); } @@ -134,26 +131,25 @@ public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionE { // Arrange var store = new Mock(); - - Idempotency.Config() - .WithPersistenceStore(store.Object) - .WithConfig(IdempotencyConfig.Builder() - .WithEventKeyJmesPath("Id") - .Build() - ).Configure(); + + Idempotency.Configure(builder => + builder + .WithPersistenceStore(store.Object) + .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) + ); IdempotencyWithErrorFunction function = new IdempotencyWithErrorFunction(); Product product = new Product(42, "fake product", 12); - + // Act Func act = async () => await function.Handle(product, new TestLambdaContext()); - + // Assert await act.Should().ThrowAsync(); store.Verify( x => x.DeleteRecord(It.IsAny(), It.IsAny())); } - + [Fact] public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() { @@ -163,19 +159,18 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() var store = new Mock(); Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); - Idempotency.Config() - .WithPersistenceStore(store.Object) - .WithConfig(IdempotencyConfig.Builder() - .WithEventKeyJmesPath("Id") - .Build() - ).Configure(); + Idempotency.Configure(builder => + builder + .WithPersistenceStore(store.Object) + .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) + ); IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); Product product = new Product(42, "fake product", 12); // Act Basket basket = await function.Handle(product, new TestLambdaContext()); - + // Assert store.Invocations.Count.Should().Be(0); basket.Products.Count.Should().Be(1); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 398172eb..4091ac2e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -76,7 +76,7 @@ public async Task SaveInProgress_WhenDefaultConfig_ShouldSaveRecordInStore() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -100,7 +100,7 @@ public async Task SaveInProgress_WhenKeyJmesPathIsSet_ShouldSaveRecordInStore_Wi var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithEventKeyJmesPath("powertools_json(Body).id") .Build(), "myfunc"); @@ -127,7 +127,7 @@ public async Task SaveInProgress_WhenJMESPath_NotFound_ShouldThrowException() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithEventKeyJmesPath("unavailable") .WithThrowOnNoIdempotencyKey(true) // should throw .Build(), ""); @@ -151,7 +151,7 @@ public async Task SaveInProgress_WhenJMESpath_NotFound_ShouldNotThrowException() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithEventKeyJmesPath("unavailable") .Build(), ""); @@ -174,7 +174,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSet_AndNotExpired_ShouldThrowEx var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithUseLocalCache(true) .WithEventKeyJmesPath("powertools_json(Body).id") .Build(), null, cache); @@ -206,7 +206,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSetButExpired_ShouldRemoveFromC var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithEventKeyJmesPath("powertools_json(Body).id") .WithUseLocalCache(true) .WithExpiration(TimeSpan.FromSeconds(2)) @@ -240,7 +240,7 @@ public async Task SaveSuccess_WhenDefaultConfig_ShouldUpdateRecord() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null, cache); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null, cache); Product product = new Product(34543, "product", 42); @@ -268,7 +268,7 @@ public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithUseLocalCache(true).Build(), null, cache); Product product = new Product(34543, "product", 42); @@ -300,7 +300,7 @@ public async Task GetRecord_WhenRecordIsInStore_ShouldReturnRecordFromPersistenc var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(), "myfunc", cache); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), "myfunc", cache); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -322,7 +322,7 @@ public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCac var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -351,7 +351,7 @@ public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRe var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -381,7 +381,7 @@ public async Task GetRecord_WhenInvalidPayload_ShouldThrowValidationException() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithEventKeyJmesPath("powertools_json(Body).id") .WithPayloadValidationJmesPath("powertools_json(Body).message") .Build(), @@ -405,7 +405,7 @@ public async Task DeleteRecord_WhenRecordExist_ShouldDeleteRecordFromPersistence var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); // Act await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); @@ -421,7 +421,7 @@ public async Task DeleteRecord_WhenLocalCacheEnabled_ShouldDeleteRecordFromCache var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(IdempotencyConfig.Builder() + persistenceStore.Configure(new IdempotencyConfigBuilder() .WithUseLocalCache(true).Build(), null, cache); cache.Set("testFunction#36e3de9a3270f82fb957c645178dfab9", @@ -443,7 +443,7 @@ public void GenerateHash_WhenInputIsString_ShouldGenerateMd5ofString() { // Arrange var persistenceStore = new InMemoryPersistenceStore(); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); string expectedHash = "70c24d88041893f7fbab4105b76fd9e1"; // MD5(Lambda rocks) // Act @@ -458,7 +458,7 @@ public void GenerateHash_WhenInputIsObject_ShouldGenerateMd5ofJsonObject() { // Arrange var persistenceStore = new InMemoryPersistenceStore(); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); Product product = new Product(42, "Product", 12); string expectedHash = "87dd2e12074c65c9bac728795a6ebb45"; // MD5({"Id":42,"Name":"Product","Price":12.0}) @@ -474,7 +474,7 @@ public void GenerateHash_WhenInputIsDouble_ShouldGenerateMd5ofDouble() { // Arrange var persistenceStore = new InMemoryPersistenceStore(); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(), null); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); string expectedHash = "bb84c94278119c8838649706df4db42b"; // MD5(256.42) // Act diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index a5fa642f..e46087c7 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -33,12 +33,11 @@ public class DynamoDBPersistenceStoreTests : IntegrationTestBase public override async Task InitializeAsync() { await base.InitializeAsync(); - _dynamoDbPersistenceStore = DynamoDBPersistenceStore - .Builder() + _dynamoDbPersistenceStore = new DynamoDBPersistenceStoreBuilder() .WithTableName(TABLE_NAME) .WithDynamoDBClient(client) .Build(); - _dynamoDbPersistenceStore.Configure(IdempotencyConfig.Builder().Build(),functionName: null); + _dynamoDbPersistenceStore.Configure(new IdempotencyConfigBuilder().Build(),functionName: null); } //putRecord [Fact] @@ -175,7 +174,7 @@ await client.PutItemAsync(new PutItemRequest Item = item }); // enable payload validation - _dynamoDbPersistenceStore.Configure(IdempotencyConfig.Builder().WithPayloadValidationJmesPath("path").Build(), + _dynamoDbPersistenceStore.Configure(new IdempotencyConfigBuilder().WithPayloadValidationJmesPath("path").Build(), null); // Act @@ -251,7 +250,7 @@ public async Task EndToEndWithCustomAttrNamesAndSortKey() BillingMode = BillingMode.PAY_PER_REQUEST }; await client.CreateTableAsync(createTableRequest); - DynamoDBPersistenceStore persistenceStore = DynamoDBPersistenceStore.Builder() + DynamoDBPersistenceStore persistenceStore = new DynamoDBPersistenceStoreBuilder() .WithTableName(TABLE_NAME_CUSTOM) .WithDynamoDBClient(client) .WithDataAttr("result") @@ -262,7 +261,7 @@ public async Task EndToEndWithCustomAttrNamesAndSortKey() .WithStatusAttr("state") .WithValidationAttr("valid") .Build(); - persistenceStore.Configure(IdempotencyConfig.Builder().Build(),functionName: null); + persistenceStore.Configure(new IdempotencyConfigBuilder().Build(),functionName: null); DateTimeOffset now = DateTimeOffset.UtcNow; DataRecord record = new DataRecord( @@ -338,7 +337,7 @@ public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() // Arrange Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); - DynamoDBPersistenceStore store = DynamoDBPersistenceStore.Builder().WithTableName(TABLE_NAME).Build(); + DynamoDBPersistenceStore store = new DynamoDBPersistenceStoreBuilder().WithTableName(TABLE_NAME).Build(); // Act Func act = () => store.GetRecord("fake"); From 5f2bf40ecc1bfa327fb3f6923e9a3a83f88f88b6 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Wed, 5 Oct 2022 16:46:14 +0400 Subject: [PATCH 07/32] Add more comments and fix null reference warnings --- docs/core/idempotency.md | 6 +- .../Core/PowertoolsLambdaContext.cs | 3 - .../Constants.cs | 20 +- .../IdempotencyAlreadyInProgressException.cs | 13 +- .../IdempotencyConfigurationException.cs | 11 + .../IdempotencyInconsistentStateException.cs | 9 + .../IdempotencyItemAlreadyExistsException.cs | 8 + .../IdempotencyItemNotFoundException.cs | 8 + .../Exceptions/IdempotencyKeyException.cs | 9 + .../IdempotencyPersistenceLayerException.cs | 8 + .../IdempotencyValidationException.cs | 9 + .../Idempotency.cs | 56 +++- .../IdempotencyOptions.cs | 250 ++++++++++-------- .../IdempotentAttribute.cs | 32 +++ .../Internal/IdempotencyHandler.cs | 6 +- .../Internal/IdempotentAspect.cs | 2 +- .../Output/ILog.cs | 4 +- .../Persistence/BasePersistenceStore.cs | 44 ++- .../Persistence/DataRecord.cs | 46 ++++ .../Persistence/DynamoDBPersistenceStore.cs | 43 ++- .../Serialization/JsonFunction.cs | 7 +- .../Internal/IdempotentAspectTests.cs | 5 +- .../Model/Basket.cs | 6 +- .../Model/Product.cs | 10 +- .../Persistence/BasePersistenceStoreTests.cs | 37 ++- .../DynamoDBPersistenceStoreTests.cs | 10 +- 26 files changed, 479 insertions(+), 183 deletions(-) diff --git a/docs/core/idempotency.md b/docs/core/idempotency.md index 591f4ded..ed49fa6b 100644 --- a/docs/core/idempotency.md +++ b/docs/core/idempotency.md @@ -191,7 +191,7 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by Idempotency behavior can be further configured with **`IdempotencyConfig`** using a builder: ```csharp -new IdempotencyConfigBuilder() +new IdempotencyOptionsBuilder() .WithEventKeyJmesPath("id") .WithPayloadValidationJmesPath("paymentId") .WithThrowOnNoIdempotencyKey(true) @@ -232,7 +232,7 @@ This is a locking mechanism for correctness. Since we don't know the result from You can enable it as seen before with: ```csharp title="Enable local cache" - new IdempotencyConfigBuilder() + new IdempotencyOptionsBuilder() .WithUseLocalCache(true) .Build() ``` @@ -251,7 +251,7 @@ In most cases, it is not desirable to store the idempotency records forever. Rat You can change this window with the **`ExpirationInSeconds`** parameter: ```csharp title="Customizing expiration time" -new IdempotencyConfigBuilder() +new IdempotencyOptionsBuilder() .WithExpiration(TimeSpan.FromMinutes(5)) .Build() ``` diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs index b07013a8..efe7c1a3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs @@ -50,9 +50,6 @@ internal class PowertoolsLambdaContext /// internal int MemoryLimitInMB { get; private set; } - /// - /// The instance - /// internal static PowertoolsLambdaContext Instance { get; private set; } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs index 61dd97c7..d3bfabf3 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs @@ -15,8 +15,20 @@ namespace AWS.Lambda.Powertools.Idempotency; -public class Constants { - public static readonly string LAMBDA_FUNCTION_NAME_ENV = "AWS_LAMBDA_FUNCTION_NAME"; - public static readonly string AWS_REGION_ENV = "AWS_REGION"; - public static readonly string IDEMPOTENCY_DISABLED_ENV = "POWERTOOLS_IDEMPOTENCY_DISABLED"; +/// +/// Class Constants +/// +internal class Constants { + /// + /// Constant for LAMBDA_FUNCTION_NAME_ENV environment variable + /// + internal const string LambdaFunctionNameEnv = "AWS_LAMBDA_FUNCTION_NAME"; + /// + /// Constant for AWS_REGION_ENV environment variable + /// + internal const string AwsRegionEnv = "AWS_REGION"; + /// + /// Constant for IDEMPOTENCY_DISABLED_ENV environment variable + /// + internal const string IdempotencyDisabledEnv = "POWERTOOLS_IDEMPOTENCY_DISABLED"; } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs index d52cb4f8..5b5f931f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs @@ -17,16 +17,27 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// This exception is thrown when the same payload is sent +/// while the previous one was not yet fully stored in the persistence layer (marked as COMPLETED). +/// Implements the +/// +/// public class IdempotencyAlreadyInProgressException: Exception { + /// + /// Creates a new IdempotencyAlreadyInProgressException + /// public IdempotencyAlreadyInProgressException() { } - + + /// public IdempotencyAlreadyInProgressException(string? message) : base(message) { } + /// public IdempotencyAlreadyInProgressException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs index 078e3f60..3c7e65d0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs @@ -17,16 +17,27 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// Exception thrown when Idempotency is not well configured: +/// - An annotated method does not return anything +/// - An annotated method does not use Task as return value +/// - An annotated method does not have parameters +/// public class IdempotencyConfigurationException : Exception { + /// + /// Creates a new IdempotencyConfigurationException + /// public IdempotencyConfigurationException() { } + /// public IdempotencyConfigurationException(string? message) : base(message) { } + /// public IdempotencyConfigurationException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs index 9267563a..5c5cab66 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs @@ -17,16 +17,25 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// Exception can happen under rare but expected cases +/// when persistent state changes in the small-time between put and get requests. +/// public class IdempotencyInconsistentStateException : Exception { + /// + /// Creates a new IdempotencyInconsistentStateException + /// public IdempotencyInconsistentStateException() { } + /// public IdempotencyInconsistentStateException(string? message) : base(message) { } + /// public IdempotencyInconsistentStateException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs index eda8ad2c..b3f95ab1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs @@ -17,16 +17,24 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// Exception thrown when trying to store an item which already exists. +/// public class IdempotencyItemAlreadyExistsException : Exception { + /// + /// Creates a new IdempotencyItemAlreadyExistsException + /// public IdempotencyItemAlreadyExistsException() { } + /// public IdempotencyItemAlreadyExistsException(string? message) : base(message) { } + /// public IdempotencyItemAlreadyExistsException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs index bf5f7d3b..bcba969c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs @@ -17,16 +17,24 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// Exception thrown when the item was not found in the persistence store. +/// public class IdempotencyItemNotFoundException : Exception { + /// + /// Creates a new IdempotencyItemNotFoundException + /// public IdempotencyItemNotFoundException() { } + /// public IdempotencyItemNotFoundException(string? message) : base(message) { } + /// public IdempotencyItemNotFoundException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs index 1bef3078..ba744111 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs @@ -17,16 +17,25 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// Exception thrown only when using +/// and if a key could not be found in the event (for example when having a bad JMESPath configured) +/// public class IdempotencyKeyException : Exception { + /// + /// Creates a new IdempotencyKeyException + /// public IdempotencyKeyException() { } + /// public IdempotencyKeyException(string? message) : base(message) { } + /// public IdempotencyKeyException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs index b288ecac..a0918c39 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs @@ -17,16 +17,24 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// Exception thrown when a technical error occurred with the persistence layer (eg. insertion, deletion, ... in database) +/// public class IdempotencyPersistenceLayerException : Exception { + /// + /// Creates a new IdempotencyPersistenceLayerException + /// public IdempotencyPersistenceLayerException() { } + /// public IdempotencyPersistenceLayerException(string? message) : base(message) { } + /// public IdempotencyPersistenceLayerException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs index 8ec387a4..ff51112e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs @@ -17,16 +17,25 @@ namespace AWS.Lambda.Powertools.Idempotency.Exceptions; +/// +/// Exception thrown only when using is configured +/// and the payload changed between two calls (but with the same idempotency key). +/// public class IdempotencyValidationException : Exception { + /// + /// Creates a new IdempotencyValidationException + /// public IdempotencyValidationException() { } + /// public IdempotencyValidationException(string? message) : base(message) { } + /// public IdempotencyValidationException(string? message, Exception? innerException) : base(message, innerException) { } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 8a2ee69f..9ed28bd8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -21,20 +21,25 @@ namespace AWS.Lambda.Powertools.Idempotency; /// /// Holds the configuration for idempotency: /// The persistence layer to use for persisting the request and response of the function (mandatory). -/// The general configuration for idempotency (optional, see {@link IdempotencyConfig.Builder} methods to see defaults values. +/// The general configurations for idempotency (optional, see {@link IdempotencyConfig.Builder} methods to see defaults values. /// Use it before the function handler get called. /// Example: Idempotency.Configure(builder => builder.WithPersistenceStore(...)); /// public class Idempotency { + /// + /// The general configurations for the idempotency + /// public IdempotencyOptions IdempotencyOptions { get; private set; } + + /// + /// The persistence layer to use for persisting the request and response of the function + /// public BasePersistenceStore PersistenceStore { get; private set; } private Idempotency() { } - - private void SetConfig(IdempotencyOptions options) { @@ -70,24 +75,38 @@ public static void Configure(Action configurationAction) } else { - Instance().SetConfig(new IdempotencyConfigBuilder().Build()); + Instance().SetConfig(new IdempotencyOptionsBuilder().Build()); } Instance().SetPersistenceStore(builder.Store); } - public class IdempotencyBuilder { - + /// + /// Create a builder that can be used to configure and create + /// + public class IdempotencyBuilder + { private IdempotencyOptions _options; private BasePersistenceStore _store; - public IdempotencyOptions Options => _options; - public BasePersistenceStore Store => _store; + internal IdempotencyOptions Options => _options; + internal BasePersistenceStore Store => _store; + /// + /// Set the persistence layer to use for storing the request and response + /// + /// + /// public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceStore) { _store = persistenceStore; return this; } + + /// + /// Configure Idempotency to use DynamoDBPersistenceStore + /// + /// The builder being used to configure the + /// public IdempotencyBuilder UseDynamoDb(Action builderAction) { DynamoDBPersistenceStoreBuilder builder = @@ -97,6 +116,11 @@ public IdempotencyBuilder UseDynamoDb(Action bu return this; } + /// + /// Configure Idempotency to use DynamoDBPersistenceStore + /// + /// The DynamoDb table name + /// public IdempotencyBuilder UseDynamoDb(string tableName) { DynamoDBPersistenceStoreBuilder builder = @@ -105,14 +129,24 @@ public IdempotencyBuilder UseDynamoDb(string tableName) return this; } - public IdempotencyBuilder WithOptions(Action builderAction) + /// + /// Set the idempotency configurations + /// + /// The builder being used to configure the . + /// + public IdempotencyBuilder WithOptions(Action builderAction) { - IdempotencyConfigBuilder builder = new IdempotencyConfigBuilder(); + IdempotencyOptionsBuilder builder = new IdempotencyOptionsBuilder(); builderAction(builder); _options = builder.Build(); return this; } - + + /// + /// Set the default idempotency configurations + /// + /// + /// public IdempotencyBuilder WithOptions(IdempotencyOptions options) { _options = options; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs index 79cc68e6..a4a3d1b0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs @@ -23,13 +23,47 @@ namespace AWS.Lambda.Powertools.Idempotency; /// public class IdempotencyOptions { + /// + /// A JMESPath expression to extract the idempotency key from the event record. + /// https://jmespath.org for more details + /// Common paths: + /// powertools_json(Body) for APIGatewayProxyRequest + /// Records[*].powertools_json(Body) for SQSEvent + /// Records[0].Sns.Message | powertools_json(@) for SNSEvent + /// Detail for ScheduledEvent (EventBridge / CloudWatch events) + /// public string EventKeyJmesPath { get; } + /// + /// JMES Path of a part of the payload to be used for validation + /// See https://jmespath.org/ + /// public string PayloadValidationJmesPath { get; } + /// + /// Boolean to indicate if we must throw an Exception when + /// idempotency key could not be found in the payload. + /// public bool ThrowOnNoIdempotencyKey { get; } + /// + /// Whether to locally cache idempotency results, by default false + /// public bool UseLocalCache { get; } + /// + /// The maximum number of items to store in local cache + /// public int LocalCacheMaxItems { get; } + /// + /// The number of seconds to wait before a record is expired + /// public long ExpirationInSeconds { get; } + /// + /// Algorithm to use for calculating hashes, + /// as supported by (eg. SHA1, SHA-256, ...) + /// public string HashFunction { get; } + + /// + /// Instance of ILog to record internal details of idempotency + /// public ILog Log { get; } internal IdempotencyOptions( @@ -52,112 +86,116 @@ internal IdempotencyOptions( Log = log; } } -public class IdempotencyConfigBuilder + +/// +/// Create a builder that can be used to configure and create +/// +public class IdempotencyOptionsBuilder +{ + private int _localCacheMaxItems = 256; + private bool _useLocalCache = false; + private long _expirationInSeconds = 60 * 60; // 1 hour + private string _eventKeyJmesPath = null; + private string _payloadValidationJmesPath; + private bool _throwOnNoIdempotencyKey = false; + private string _hashFunction = "MD5"; + private ILog _log = new ConsoleLog(); + + /// + /// Initialize and return an instance of IdempotencyConfig. + /// Example: + /// IdempotencyConfig.Builder().WithUseLocalCache().Build(); + /// This instance must then be passed to the Idempotency.Config: + /// Idempotency.Config().WithConfig(config).Configure(); + /// + /// an instance of IdempotencyConfig + public IdempotencyOptions Build() => + new IdempotencyOptions(_eventKeyJmesPath, + _payloadValidationJmesPath, + _throwOnNoIdempotencyKey, + _useLocalCache, + _localCacheMaxItems, + _expirationInSeconds, + _hashFunction, + _log); + + /// + /// A JMESPath expression to extract the idempotency key from the event record. + /// See https://jmespath.org/ for more details. + /// + /// path of the key in the Lambda event + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithEventKeyJmesPath(string eventKeyJmesPath) { - private int _localCacheMaxItems = 256; - private bool _useLocalCache = false; - private long _expirationInSeconds = 60 * 60; // 1 hour - private string _eventKeyJmesPath = null; - private string _payloadValidationJmesPath; - private bool _throwOnNoIdempotencyKey = false; - private string _hashFunction = "MD5"; - private ILog _log = new ConsoleLog(); - - /// - /// Initialize and return an instance of IdempotencyConfig. - /// Example: - /// IdempotencyConfig.Builder().WithUseLocalCache().Build(); - /// This instance must then be passed to the Idempotency.Config: - /// Idempotency.Config().WithConfig(config).Configure(); - /// - /// an instance of IdempotencyConfig - public IdempotencyOptions Build() => - new IdempotencyOptions(_eventKeyJmesPath, - _payloadValidationJmesPath, - _throwOnNoIdempotencyKey, - _useLocalCache, - _localCacheMaxItems, - _expirationInSeconds, - _hashFunction, - _log); + _eventKeyJmesPath = eventKeyJmesPath; + return this; + } + + /// + /// Whether to locally cache idempotency results, by default false + /// + /// Indicate if a local cache must be used in addition to the persistence store. + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithUseLocalCache(bool useLocalCache) + { + _useLocalCache = useLocalCache; + return this; + } - /// - /// A JMESPath expression to extract the idempotency key from the event record. - /// See https://jmespath.org/ for more details. - /// - /// path of the key in the Lambda event - /// the instance of the builder (to chain operations) - public IdempotencyConfigBuilder WithEventKeyJmesPath(string eventKeyJmesPath) - { - _eventKeyJmesPath = eventKeyJmesPath; - return this; - } + /// + /// A JMESPath expression to extract the payload to be validated from the event record. + /// See https://jmespath.org/ for more details. + /// + /// JMES Path of a part of the payload to be used for validation + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithPayloadValidationJmesPath(string payloadValidationJmesPath) + { + _payloadValidationJmesPath = payloadValidationJmesPath; + return this; + } - /// - /// Whether to locally cache idempotency results, by default false - /// - /// Indicate if a local cache must be used in addition to the persistence store. - /// the instance of the builder (to chain operations) - public IdempotencyConfigBuilder WithUseLocalCache(bool useLocalCache) - { - _useLocalCache = useLocalCache; - return this; - } - - /// - /// A JMESPath expression to extract the payload to be validated from the event record. - /// See https://jmespath.org/ for more details. - /// - /// JMES Path of a part of the payload to be used for validation - /// the instance of the builder (to chain operations) - public IdempotencyConfigBuilder WithPayloadValidationJmesPath(string payloadValidationJmesPath) - { - _payloadValidationJmesPath = payloadValidationJmesPath; - return this; - } - - /// - /// Whether to throw an exception if no idempotency key was found in the request, by default false - /// - /// boolean to indicate if we must throw an Exception when - /// idempotency key could not be found in the payload. - /// the instance of the builder (to chain operations) - public IdempotencyConfigBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdempotencyKey) - { - _throwOnNoIdempotencyKey = throwOnNoIdempotencyKey; - return this; - } - - /// - /// The number of seconds to wait before a record is expired - /// - /// expiration of the record in the store - /// the instance of the builder (to chain operations) - public IdempotencyConfigBuilder WithExpiration(TimeSpan duration) - { - _expirationInSeconds = (long)duration.TotalSeconds; - return this; - } - - /// - /// Function to use for calculating hashes, by default MD5. - /// - /// Can be any algorithm supported by HashAlgorithm.Create - /// the instance of the builder (to chain operations) - public IdempotencyConfigBuilder WithHashFunction(string hashFunction) - { - _hashFunction = hashFunction; - return this; - } - - /// - /// Logs to a custom logger. - /// - /// The logger. - /// the instance of the builder (to chain operations) - public IdempotencyConfigBuilder LogTo(ILog log) - { - _log = log; - return this; - } - } \ No newline at end of file + /// + /// Whether to throw an exception if no idempotency key was found in the request, by default false + /// + /// boolean to indicate if we must throw an Exception when + /// idempotency key could not be found in the payload. + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdempotencyKey) + { + _throwOnNoIdempotencyKey = throwOnNoIdempotencyKey; + return this; + } + + /// + /// The number of seconds to wait before a record is expired + /// + /// expiration of the record in the store + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration) + { + _expirationInSeconds = (long) duration.TotalSeconds; + return this; + } + + /// + /// Function to use for calculating hashes, by default MD5. + /// + /// Can be any algorithm supported by HashAlgorithm.Create + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithHashFunction(string hashFunction) + { + _hashFunction = hashFunction; + return this; + } + + /// + /// Logs to a custom logger. + /// + /// The logger. + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder LogTo(ILog log) + { + _log = log; + return this; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 52f3f529..ab58a17b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -19,6 +19,38 @@ namespace AWS.Lambda.Powertools.Idempotency; +/// +/// Idempotent is used to signal that the annotated method is idempotent: +/// Calling this method one or multiple times with the same parameter will always return the same result. +/// This annotation can be placed on any method of a Lambda function +/// +/// [Idempotent] +/// public Task<string> FunctionHandler(string input, ILambdaContext context) +/// { +/// return Task.FromResult(input.ToUpper()); +/// } +/// +/// Environment variables
+/// ---------------------
+/// +/// +/// Variable name +/// Description +/// +/// +/// AWS_LAMBDA_FUNCTION_NAME +/// string, function name +/// +/// +/// AWS_REGION +/// string, AWS region +/// +/// +/// POWERTOOLS_IDEMPOTENCY_DISABLED +/// string, Enable or disable the Idempotency +/// +/// +///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] [Injection(typeof(IdempotentAspect), Inherited = true)] public class IdempotentAttribute : Attribute diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs index ab100a9f..607ea871 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -23,9 +23,9 @@ namespace AWS.Lambda.Powertools.Idempotency.Internal; -public class IdempotencyHandler +internal class IdempotencyHandler { - private static readonly int MAX_RETRIES = 2; + private const int MaxRetries = 2; private readonly Func _target; private readonly object[] _args; @@ -65,7 +65,7 @@ public async Task Handle() } catch (IdempotencyInconsistentStateException) { - if (i == MAX_RETRIES) + if (i == MaxRetries) { throw; } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs index 93daa37d..6c43c84d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs @@ -48,7 +48,7 @@ public object Handle( private static async Task WrapAsync(Func target, object[] args, MethodBase method) { - string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV); + string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); if (idempotencyDisabledEnv is "true") { return await (Task)target(args); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs index 8696fc07..14b35ca1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs @@ -13,9 +13,11 @@ * permissions and limitations under the License. */ -//TODO: Replace with core PowerTools logging namespace AWS.Lambda.Powertools.Idempotency.Output; +/// +/// Implemented by objects which record internal details of idempotency +/// public interface ILog { /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 7b61e33c..0d31190b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -27,17 +27,33 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; +/// +/// Persistence layer that will store the idempotency result. +/// Base implementation. See for an implementation (default one) +/// Extends this class to use your own implementation (DocumentDB, Elasticache, ...) +/// public abstract class BasePersistenceStore : IPersistenceStore { - private IdempotencyOptions _idempotencyOptions; + private IdempotencyOptions _idempotencyOptions = null!; private string? _functionName; + /// + /// Boolean to indicate whether or not payload validation is enabled + /// protected bool PayloadValidationEnabled; private LRUCache _cache = null!; - protected ILog Log; + /// + /// Instance of ILog to log the internal details of idempotency + /// + protected ILog Log = null!; + /// + /// Initialize the base persistence layer from the configuration settings + /// + /// Idempotency configuration settings + /// The name of the function being decorated public void Configure(IdempotencyOptions idempotencyOptions, string? functionName) { - string? funcEnv = Environment.GetEnvironmentVariable(Constants.LAMBDA_FUNCTION_NAME_ENV); + string? funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv); _functionName = funcEnv != null ? funcEnv : "testFunction"; if (!string.IsNullOrWhiteSpace(functionName)) { @@ -68,6 +84,12 @@ internal void Configure(IdempotencyOptions options, string functionName, LRUCach _cache = cache; } + /// + /// Save record of function's execution completing successfully + /// + /// Payload + /// the response from the function + /// The current date time public virtual async Task SaveSuccess(JToken data, object result, DateTimeOffset now) { string responseJson = JsonConvert.SerializeObject(result); @@ -83,6 +105,12 @@ public virtual async Task SaveSuccess(JToken data, object result, DateTimeOffset SaveToCache(record); } + /// + /// Save record of function's execution being in progress + /// + /// Payload + /// The current date time + /// public virtual async Task SaveInProgress(JToken data, DateTimeOffset now) { string idempotencyKey = GetHashedIdempotencyKey(data); @@ -275,6 +303,12 @@ private bool IsMissingIdemPotencyKey(JToken data) (data.Type == JTokenType.Null); } + /// + /// Generate a hash value from the provided data + /// + /// data to hash + /// Hashed representation of the provided data + /// internal string GenerateHash(JToken data) { JToken node; @@ -332,11 +366,15 @@ private static string GetHash(HashAlgorithm hashAlgorithm, string input) return sBuilder.ToString(); } + /// public abstract Task GetRecord(string idempotencyKey); + /// public abstract Task PutRecord(DataRecord record, DateTimeOffset now); + /// public abstract Task UpdateRecord(DataRecord record); + /// public abstract Task DeleteRecord(string idempotencyKey); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs index 3008c2ce..043942d4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs @@ -17,10 +17,21 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; +/// +/// Data Class for idempotency records. This is actually the item that will be stored in the persistence layer. +/// public class DataRecord { private readonly string _status; + /// + /// Creates a new DataRecord + /// + /// Hash representation of either entire event or specific configured subject of the event + /// The DataRecordStatus + /// Unix timestamp of when record expires + /// JSON serialized invocation results + /// A hash representation of the entire event public DataRecord(string idempotencyKey, DataRecordStatus status, long expiryTimestamp, @@ -34,9 +45,21 @@ public DataRecord(string idempotencyKey, PayloadHash = payloadHash; } + /// + /// A hash representation of either the entire event or a specific configured subset of the event + /// public string IdempotencyKey { get; } + /// + /// Unix timestamp of when record expires + /// public long ExpiryTimestamp { get; } + /// + /// JSON serialized invocation results + /// public string? ResponseData { get; } + /// + /// A hash representation of the entire event + /// public string? PayloadHash { get; } @@ -50,6 +73,10 @@ public bool IsExpired(DateTimeOffset now) return ExpiryTimestamp != 0 && now.ToUnixTimeSeconds() > ExpiryTimestamp; } + + /// + /// Represents the Status + /// public DataRecordStatus Status { get @@ -64,6 +91,11 @@ public DataRecordStatus Status } } + /// + /// Determines whether the specified DataRecord is equal to the current DataRecord + /// + /// The DataRecord to compare with the current object. + /// true if the specified DataRecord is equal to the current DataRecord; otherwise, false. protected bool Equals(DataRecord other) { return _status == other._status @@ -73,6 +105,7 @@ protected bool Equals(DataRecord other) && PayloadHash == other.PayloadHash; } + /// public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; @@ -81,6 +114,7 @@ public override bool Equals(object? obj) return Equals((DataRecord) obj); } + /// public override int GetHashCode() { return HashCode.Combine(IdempotencyKey, _status, ExpiryTimestamp, ResponseData, PayloadHash); @@ -93,8 +127,20 @@ public override int GetHashCode() /// -- EXPIRED: record expired, idempotency will not happen /// public enum DataRecordStatus { + /// + /// record initialized when function starts + /// + // ReSharper disable once InconsistentNaming INPROGRESS, + /// + /// record updated with the result of the function when it ends + /// + // ReSharper disable once InconsistentNaming COMPLETED, + /// + /// record expired, idempotency will not happen + /// + // ReSharper disable once InconsistentNaming EXPIRED } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index 130357e2..cb32c9b0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -23,6 +23,10 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; +/// +/// DynamoDB version of the . Will store idempotency data in DynamoDB. +/// +// ReSharper disable once InconsistentNaming public class DynamoDBPersistenceStore : BasePersistenceStore { private readonly string _tableName; @@ -33,7 +37,7 @@ public class DynamoDBPersistenceStore : BasePersistenceStore private readonly string _statusAttr; private readonly string _dataAttr; private readonly string _validationAttr; - private readonly AmazonDynamoDBClient _dynamoDbClient; + private readonly AmazonDynamoDBClient? _dynamoDbClient; internal DynamoDBPersistenceStore(string tableName, string keyAttr, @@ -43,7 +47,7 @@ internal DynamoDBPersistenceStore(string tableName, string statusAttr, string dataAttr, string validationAttr, - AmazonDynamoDBClient client) + AmazonDynamoDBClient? client) { _tableName = tableName; _keyAttr = keyAttr; @@ -60,12 +64,12 @@ internal DynamoDBPersistenceStore(string tableName, } else { - string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV); + string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); if (idempotencyDisabledEnv == null || idempotencyDisabledEnv.Equals("false")) { AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig { - RegionEndpoint = RegionEndpoint.GetBySystemName(Environment.GetEnvironmentVariable(Constants.AWS_REGION_ENV)) + RegionEndpoint = RegionEndpoint.GetBySystemName(Environment.GetEnvironmentVariable(Constants.AwsRegionEnv)) }; _dynamoDbClient = new AmazonDynamoDBClient(clientConfig); } else { @@ -77,6 +81,7 @@ internal DynamoDBPersistenceStore(string tableName, } + /// public override async Task GetRecord(string idempotencyKey) { var getItemRequest = new GetItemRequest() @@ -85,7 +90,7 @@ public override async Task GetRecord(string idempotencyKey) ConsistentRead = true, Key = GetKey(idempotencyKey) }; - GetItemResponse response = await _dynamoDbClient.GetItemAsync(getItemRequest); + GetItemResponse response = await _dynamoDbClient!.GetItemAsync(getItemRequest); if (!response.IsItemSet) { @@ -96,6 +101,7 @@ public override async Task GetRecord(string idempotencyKey) } + /// public override async Task PutRecord(DataRecord record, DateTimeOffset now) { Dictionary item = new(GetKey(record.IdempotencyKey)); @@ -131,7 +137,7 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) {":now", new AttributeValue() {N = now.ToUnixTimeSeconds().ToString()}} } }; - await _dynamoDbClient.PutItemAsync(request); + await _dynamoDbClient!.PutItemAsync(request); } catch (ConditionalCheckFailedException e) { @@ -140,8 +146,9 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) "Failed to put record for already existing idempotency key: " + record.IdempotencyKey, e); } } - - + + + /// public override async Task UpdateRecord(DataRecord record) { Log.WriteDebug("Updating record for idempotency key: {0}", record.IdempotencyKey); @@ -176,9 +183,10 @@ public override async Task UpdateRecord(DataRecord record) ExpressionAttributeNames = expressionAttributeNames, ExpressionAttributeValues = expressionAttributeValues }; - await _dynamoDbClient.UpdateItemAsync(request); + await _dynamoDbClient!.UpdateItemAsync(request); } - + + /// public override async Task DeleteRecord(string idempotencyKey) { Log.WriteDebug("Deleting record for idempotency key: {0}", idempotencyKey); @@ -187,7 +195,7 @@ public override async Task DeleteRecord(string idempotencyKey) TableName = _tableName, Key = GetKey(idempotencyKey) }; - await _dynamoDbClient.DeleteItemAsync(request); + await _dynamoDbClient!.DeleteItemAsync(request); } /// @@ -231,9 +239,16 @@ private Dictionary GetKey(string idempotencyKey) } +/// +/// Use this builder to get an instance of .
+/// With this builder you can configure the characteristics of the DynamoDB Table +/// (name, key, sort key, and other field names).
+/// You can also set a custom AmazonDynamoDBClient for further tuning. +///
+// ReSharper disable once InconsistentNaming public class DynamoDBPersistenceStoreBuilder { - private static readonly string? FuncEnv = Environment.GetEnvironmentVariable(Constants.LAMBDA_FUNCTION_NAME_ENV); + private static readonly string? FuncEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv); private string _tableName = null!; private string _keyAttr = "id"; @@ -243,7 +258,7 @@ public class DynamoDBPersistenceStoreBuilder private string _statusAttr = "status"; private string _dataAttr = "data"; private string _validationAttr = "validation"; - private AmazonDynamoDBClient _dynamoDbClient; + private AmazonDynamoDBClient? _dynamoDbClient; /// /// Initialize and return a new instance of {@link DynamoDBPersistenceStore}. @@ -256,7 +271,7 @@ public DynamoDBPersistenceStore Build() { if (string.IsNullOrWhiteSpace(_tableName)) { - throw new ArgumentNullException("Table name is not specified"); + throw new ArgumentNullException($"Table name is not specified"); } return new DynamoDBPersistenceStore(_tableName, diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs index 484cc65e..f4be86e7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs @@ -18,13 +18,18 @@ namespace AWS.Lambda.Powertools.Idempotency.Serialization; +/// +/// Creates JMESPath function powertools_json() to treat the payload as a JSON object rather than a string. +/// public class JsonFunction : JmesPathFunction { - public JsonFunction() + /// + public JsonFunction() : base("powertools_json", 1) { } + /// public override JToken Execute(params JmesPathFunctionArgument[] args) { System.Diagnostics.Debug.Assert(args.Length == 1); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index b6b7197c..a8897b76 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -14,6 +14,7 @@ */ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Idempotency.Exceptions; @@ -157,7 +158,7 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() { // Arrange var store = new Mock(); - Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); Idempotency.Configure(builder => builder @@ -178,7 +179,7 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() } finally { - Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "false"); + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs index a34c090a..919d3b22 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs @@ -14,6 +14,7 @@ */ using System.Collections.Generic; +using System.Linq; namespace AWS.Lambda.Powertools.Idempotency.Tests.Model; @@ -37,11 +38,10 @@ public void Add(Product product) protected bool Equals(Basket other) { - var products = Products; - return products.Equals(products); + return Products.All(other.Products.Contains); } - public override bool Equals(object? obj) + public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs index d05c11ad..e1bd54e1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs @@ -17,7 +17,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Model; -public class Product +public class Product : IEquatable { public long Id { get; } public string Name { get; } @@ -30,12 +30,16 @@ public Product(long id, string name, double price) Price = price; } - protected bool Equals(Product other) + public bool Equals(Product other) { + if (other == null) + { + return false; + } return Id == other.Id && Name == other.Name && Price.Equals(other.Price); } - public override bool Equals(object? obj) + public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 4091ac2e..41beec18 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -76,7 +76,7 @@ public async Task SaveInProgress_WhenDefaultConfig_ShouldSaveRecordInStore() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -100,7 +100,7 @@ public async Task SaveInProgress_WhenKeyJmesPathIsSet_ShouldSaveRecordInStore_Wi var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithEventKeyJmesPath("powertools_json(Body).id") .Build(), "myfunc"); @@ -127,7 +127,7 @@ public async Task SaveInProgress_WhenJMESPath_NotFound_ShouldThrowException() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithEventKeyJmesPath("unavailable") .WithThrowOnNoIdempotencyKey(true) // should throw .Build(), ""); @@ -151,7 +151,7 @@ public async Task SaveInProgress_WhenJMESpath_NotFound_ShouldNotThrowException() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithEventKeyJmesPath("unavailable") .Build(), ""); @@ -174,7 +174,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSet_AndNotExpired_ShouldThrowEx var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true) .WithEventKeyJmesPath("powertools_json(Body).id") .Build(), null, cache); @@ -206,7 +206,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSetButExpired_ShouldRemoveFromC var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithEventKeyJmesPath("powertools_json(Body).id") .WithUseLocalCache(true) .WithExpiration(TimeSpan.FromSeconds(2)) @@ -240,7 +240,7 @@ public async Task SaveSuccess_WhenDefaultConfig_ShouldUpdateRecord() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null, cache); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null, cache); Product product = new Product(34543, "product", 42); @@ -268,7 +268,7 @@ public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), null, cache); Product product = new Product(34543, "product", 42); @@ -300,7 +300,7 @@ public async Task GetRecord_WhenRecordIsInStore_ShouldReturnRecordFromPersistenc var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), "myfunc", cache); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), "myfunc", cache); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -322,7 +322,7 @@ public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCac var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -351,7 +351,7 @@ public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRe var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); DateTimeOffset now = DateTimeOffset.UtcNow; @@ -381,13 +381,12 @@ public async Task GetRecord_WhenInvalidPayload_ShouldThrowValidationException() var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithEventKeyJmesPath("powertools_json(Body).id") .WithPayloadValidationJmesPath("powertools_json(Body).message") .Build(), "myfunc"); - - var validationHash = "different hash"; // "Lambda rocks" ==> 70c24d88041893f7fbab4105b76fd9e1 + DateTimeOffset now = DateTimeOffset.UtcNow; // Act @@ -405,7 +404,7 @@ public async Task DeleteRecord_WhenRecordExist_ShouldDeleteRecordFromPersistence var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); // Act await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); @@ -421,7 +420,7 @@ public async Task DeleteRecord_WhenLocalCacheEnabled_ShouldDeleteRecordFromCache var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); LRUCache cache = new ((int) 2); - persistenceStore.Configure(new IdempotencyConfigBuilder() + persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), null, cache); cache.Set("testFunction#36e3de9a3270f82fb957c645178dfab9", @@ -443,7 +442,7 @@ public void GenerateHash_WhenInputIsString_ShouldGenerateMd5ofString() { // Arrange var persistenceStore = new InMemoryPersistenceStore(); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); string expectedHash = "70c24d88041893f7fbab4105b76fd9e1"; // MD5(Lambda rocks) // Act @@ -458,7 +457,7 @@ public void GenerateHash_WhenInputIsObject_ShouldGenerateMd5ofJsonObject() { // Arrange var persistenceStore = new InMemoryPersistenceStore(); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); Product product = new Product(42, "Product", 12); string expectedHash = "87dd2e12074c65c9bac728795a6ebb45"; // MD5({"Id":42,"Name":"Product","Price":12.0}) @@ -474,7 +473,7 @@ public void GenerateHash_WhenInputIsDouble_ShouldGenerateMd5ofDouble() { // Arrange var persistenceStore = new InMemoryPersistenceStore(); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(), null); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); string expectedHash = "bb84c94278119c8838649706df4db42b"; // MD5(256.42) // Act diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index e46087c7..dfe5a9fc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -37,7 +37,7 @@ public override async Task InitializeAsync() .WithTableName(TABLE_NAME) .WithDynamoDBClient(client) .Build(); - _dynamoDbPersistenceStore.Configure(new IdempotencyConfigBuilder().Build(),functionName: null); + _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); } //putRecord [Fact] @@ -174,7 +174,7 @@ await client.PutItemAsync(new PutItemRequest Item = item }); // enable payload validation - _dynamoDbPersistenceStore.Configure(new IdempotencyConfigBuilder().WithPayloadValidationJmesPath("path").Build(), + _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().WithPayloadValidationJmesPath("path").Build(), null); // Act @@ -261,7 +261,7 @@ public async Task EndToEndWithCustomAttrNamesAndSortKey() .WithStatusAttr("state") .WithValidationAttr("valid") .Build(); - persistenceStore.Configure(new IdempotencyConfigBuilder().Build(),functionName: null); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); DateTimeOffset now = DateTimeOffset.UtcNow; DataRecord record = new DataRecord( @@ -335,7 +335,7 @@ public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() try { // Arrange - Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "true"); + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); DynamoDBPersistenceStore store = new DynamoDBPersistenceStoreBuilder().WithTableName(TABLE_NAME).Build(); @@ -347,7 +347,7 @@ public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() } finally { - Environment.SetEnvironmentVariable(Constants.IDEMPOTENCY_DISABLED_ENV, "false"); + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); } } private static Dictionary CreateKey(string keyValue) From 127f52c2a6822fb50b469828c700de4aecd65785 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Wed, 5 Oct 2022 17:06:37 +0400 Subject: [PATCH 08/32] Add NullLog as default logger --- .../IdempotencyOptions.cs | 2 +- .../Output/NullLog.cs | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs index a4a3d1b0..d7bc92db 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs @@ -99,7 +99,7 @@ public class IdempotencyOptionsBuilder private string _payloadValidationJmesPath; private bool _throwOnNoIdempotencyKey = false; private string _hashFunction = "MD5"; - private ILog _log = new ConsoleLog(); + private ILog _log = new NullLog(); /// /// Initialize and return an instance of IdempotencyConfig. diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs new file mode 100644 index 00000000..f58fc688 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs @@ -0,0 +1,40 @@ +namespace AWS.Lambda.Powertools.Idempotency.Output; + +/// +/// Does not write any log messages, all method simply return +/// +public class NullLog: ILog +{ + /// + /// Does not write information message, simply return + /// + /// + /// + public void WriteInformation(string format, params object[] args) + { } + + /// + /// Does not write error message, simply return + /// + /// + /// + public void WriteError(string format, params object[] args) + { } + + + /// + /// Does not write warning message, simply return + /// + /// + /// + public void WriteWarning(string format, params object[] args) + { } + + /// + /// Does not write debug message, simply return + /// + /// + /// + public void WriteDebug(string format, params object[] args) + { } +} \ No newline at end of file From adcbe974e8dc1458b995278bcbb75660d46806ec Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Thu, 6 Oct 2022 09:25:18 +0400 Subject: [PATCH 09/32] Use UniversalWrapperAspect as opposed to custom IdempotentAspect --- .../IdempotentAttribute.cs | 35 +++++++++- .../{ => Internal}/Constants.cs | 2 +- .../Internal/IdempotentAspect.cs | 69 ------------------- .../Persistence/DynamoDBPersistenceStore.cs | 1 + .../Internal/IdempotentAspectTests.cs | 1 + .../DynamoDBPersistenceStoreTests.cs | 1 + 6 files changed, 36 insertions(+), 73 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Idempotency/{ => Internal}/Constants.cs (95%) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index ab58a17b..498bb6dd 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -14,8 +14,12 @@ */ using System; +using System.Threading.Tasks; using AspectInjector.Broker; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; +using Newtonsoft.Json.Linq; namespace AWS.Lambda.Powertools.Idempotency; @@ -51,8 +55,33 @@ namespace AWS.Lambda.Powertools.Idempotency; /// /// /// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -[Injection(typeof(IdempotentAspect), Inherited = true)] -public class IdempotentAttribute : Attribute +[AttributeUsage(AttributeTargets.Method)] +[Injection(typeof(UniversalWrapperAspect), Inherited = true)] +public class IdempotentAttribute : UniversalWrapperAttribute { + protected sealed override T WrapSync(Func target, object[] args, AspectEventArgs eventArgs) + { + throw new IdempotencyConfigurationException("Idempotent attribute can be used on async methods only"); + } + + protected sealed override async Task WrapAsync(Func> target, object[] args, + AspectEventArgs eventArgs) + { + string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); + if (idempotencyDisabledEnv is "true") + { + return await (Task)target(args); + } + JToken payload = JToken.FromObject(args[0]); + if (payload == null) + { + throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); + } + + var types = new[] {typeof(T)}; + var genericType = typeof(IdempotencyHandler<>).MakeGenericType(types); + var idempotencyHandler = (IdempotencyHandler)Activator.CreateInstance(genericType,target, args, eventArgs.Method.Name, payload); + var result = await idempotencyHandler.Handle(); + return result; + } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs similarity index 95% rename from libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs rename to libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs index d3bfabf3..90f8578a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -namespace AWS.Lambda.Powertools.Idempotency; +namespace AWS.Lambda.Powertools.Idempotency.Internal; /// /// Class Constants diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs deleted file mode 100644 index 6c43c84d..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotentAspect.cs +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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.Reflection; -using System.Threading.Tasks; -using AspectInjector.Broker; -using AWS.Lambda.Powertools.Idempotency.Exceptions; -using Newtonsoft.Json.Linq; - -namespace AWS.Lambda.Powertools.Idempotency.Internal; - -[Aspect(Scope.Global)] -public class IdempotentAspect -{ - private static MethodInfo _asyncErrorHandler = typeof(IdempotentAspect).GetMethod(nameof(WrapAsync), BindingFlags.NonPublic | BindingFlags.Static)!; - - [Advice(Kind.Around, Targets = Target.Method)] - public object Handle( - [Argument(Source.Target)] Func target, - [Argument(Source.Arguments)] object[] args, - [Argument(Source.Instance)] object instance, - [Argument(Source.ReturnType)] Type retType, - [Argument(Source.Triggers)] Attribute[] triggers, - [Argument(Source.Metadata)] MethodBase method - ) - { - if (!typeof(Task).IsAssignableFrom((Type?) retType)) - { - throw new IdempotencyConfigurationException("Invalid Target Exception"); - } - - var syncResultType = retType.IsConstructedGenericType ? retType.GenericTypeArguments[0] : typeof(Task); - return _asyncErrorHandler.MakeGenericMethod(syncResultType).Invoke(this, new object[] { target, args, method }); - } - - private static async Task WrapAsync(Func target, object[] args, MethodBase method) - { - string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); - if (idempotencyDisabledEnv is "true") - { - return await (Task)target(args); - } - JToken payload = JToken.FromObject(args[0]); - if (payload == null) - { - throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); - } - - var types = new[] {typeof(T)}; - var genericType = typeof(IdempotencyHandler<>).MakeGenericType(types); - var idempotencyHandler = (IdempotencyHandler)Activator.CreateInstance(genericType,target, args, method.Name, payload); - var result = await idempotencyHandler.Handle(); - return result; - - } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index cb32c9b0..b96e7557 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -20,6 +20,7 @@ using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Internal; namespace AWS.Lambda.Powertools.Idempotency.Persistence; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index a8897b76..4baa2e0d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -18,6 +18,7 @@ using System.Threading.Tasks; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Internal; using AWS.Lambda.Powertools.Idempotency.Persistence; using AWS.Lambda.Powertools.Idempotency.Tests.Handlers; using AWS.Lambda.Powertools.Idempotency.Tests.Model; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index dfe5a9fc..da6efd0b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -20,6 +20,7 @@ using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Internal; using AWS.Lambda.Powertools.Idempotency.Persistence; using FluentAssertions; using Xunit; From 7e1e7eef9802a2853cb1ff7852f87a67346ca8f6 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Thu, 6 Oct 2022 10:00:11 +0400 Subject: [PATCH 10/32] Add missing documentation --- .../IdempotentAttribute.cs | 29 ++++++++++++++++--- .../Internal/IdempotencyHandler.cs | 1 - 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 498bb6dd..4351bc34 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -59,18 +59,35 @@ namespace AWS.Lambda.Powertools.Idempotency; [Injection(typeof(UniversalWrapperAspect), Inherited = true)] public class IdempotentAttribute : UniversalWrapperAttribute { + /// + /// Wraps as a synchronous operation, simply throws IdempotencyConfigurationException + /// + /// + /// The target. + /// The arguments. + /// The instance containing the event data. + /// T. protected sealed override T WrapSync(Func target, object[] args, AspectEventArgs eventArgs) { throw new IdempotencyConfigurationException("Idempotent attribute can be used on async methods only"); } - protected sealed override async Task WrapAsync(Func> target, object[] args, - AspectEventArgs eventArgs) + /// + /// Wrap as an asynchronous operation. + /// + /// + /// The target. + /// The arguments. + /// The instance containing the event data. + /// A Task<T> representing the asynchronous operation. + protected sealed override async Task WrapAsync( + Func> target, object[] args, AspectEventArgs eventArgs) { + string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); if (idempotencyDisabledEnv is "true") { - return await (Task)target(args); + return await base.WrapAsync(target, args, eventArgs); } JToken payload = JToken.FromObject(args[0]); if (payload == null) @@ -80,7 +97,11 @@ protected sealed override async Task WrapAsync(Func> tar var types = new[] {typeof(T)}; var genericType = typeof(IdempotencyHandler<>).MakeGenericType(types); - var idempotencyHandler = (IdempotencyHandler)Activator.CreateInstance(genericType,target, args, eventArgs.Method.Name, payload); + var idempotencyHandler = Activator.CreateInstance(genericType,target, args, eventArgs.Method.Name, payload) as IdempotencyHandler; + if (idempotencyHandler == null) + { + throw new Exception("Failed to create an instance of IdempotencyHandler"); + } var result = await idempotencyHandler.Handle(); return result; } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs index 607ea871..85ee2182 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -80,7 +80,6 @@ public async Task Handle() /// private async Task ProcessIdempotency() { - try { // We call saveInProgress first as an optimization for the most common case where no idempotent record From ef81092dac92f357aff6c320ba32e560e274160a Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Thu, 6 Oct 2022 10:36:34 +0400 Subject: [PATCH 11/32] Fix Null Reference warning --- .../Idempotency.cs | 14 +- .../IdempotencyOptions.cs | 122 +----------------- .../IdempotencyOptionsBuilder.cs | 117 +++++++++++++++++ .../Internal/IdempotencyHandler.cs | 8 +- .../Persistence/BasePersistenceStore.cs | 2 +- 5 files changed, 135 insertions(+), 128 deletions(-) create mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 9ed28bd8..83ec2324 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -30,12 +30,12 @@ public class Idempotency /// /// The general configurations for the idempotency /// - public IdempotencyOptions IdempotencyOptions { get; private set; } - + public IdempotencyOptions IdempotencyOptions { get; private set; } = null!; + /// /// The persistence layer to use for persisting the request and response of the function /// - public BasePersistenceStore PersistenceStore { get; private set; } + public BasePersistenceStore PersistenceStore { get; private set; } = null!; private Idempotency() { @@ -85,11 +85,11 @@ public static void Configure(Action configurationAction) /// public class IdempotencyBuilder { - private IdempotencyOptions _options; - private BasePersistenceStore _store; + private IdempotencyOptions? _options; + private BasePersistenceStore? _store; - internal IdempotencyOptions Options => _options; - internal BasePersistenceStore Store => _store; + internal IdempotencyOptions? Options => _options; + internal BasePersistenceStore? Store => _store; /// /// Set the persistence layer to use for storing the request and response diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs index d7bc92db..cfbc60b0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -using System; using AWS.Lambda.Powertools.Idempotency.Output; namespace AWS.Lambda.Powertools.Idempotency; @@ -32,12 +31,12 @@ public class IdempotencyOptions /// Records[0].Sns.Message | powertools_json(@) for SNSEvent /// Detail for ScheduledEvent (EventBridge / CloudWatch events) /// - public string EventKeyJmesPath { get; } + public string? EventKeyJmesPath { get; } /// /// JMES Path of a part of the payload to be used for validation /// See https://jmespath.org/ /// - public string PayloadValidationJmesPath { get; } + public string? PayloadValidationJmesPath { get; } /// /// Boolean to indicate if we must throw an Exception when /// idempotency key could not be found in the payload. @@ -67,8 +66,8 @@ public class IdempotencyOptions public ILog Log { get; } internal IdempotencyOptions( - string eventKeyJmesPath, - string payloadValidationJmesPath, + string? eventKeyJmesPath, + string? payloadValidationJmesPath, bool throwOnNoIdempotencyKey, bool useLocalCache, int localCacheMaxItems, @@ -85,117 +84,4 @@ internal IdempotencyOptions( HashFunction = hashFunction; Log = log; } -} - -/// -/// Create a builder that can be used to configure and create -/// -public class IdempotencyOptionsBuilder -{ - private int _localCacheMaxItems = 256; - private bool _useLocalCache = false; - private long _expirationInSeconds = 60 * 60; // 1 hour - private string _eventKeyJmesPath = null; - private string _payloadValidationJmesPath; - private bool _throwOnNoIdempotencyKey = false; - private string _hashFunction = "MD5"; - private ILog _log = new NullLog(); - - /// - /// Initialize and return an instance of IdempotencyConfig. - /// Example: - /// IdempotencyConfig.Builder().WithUseLocalCache().Build(); - /// This instance must then be passed to the Idempotency.Config: - /// Idempotency.Config().WithConfig(config).Configure(); - /// - /// an instance of IdempotencyConfig - public IdempotencyOptions Build() => - new IdempotencyOptions(_eventKeyJmesPath, - _payloadValidationJmesPath, - _throwOnNoIdempotencyKey, - _useLocalCache, - _localCacheMaxItems, - _expirationInSeconds, - _hashFunction, - _log); - - /// - /// A JMESPath expression to extract the idempotency key from the event record. - /// See https://jmespath.org/ for more details. - /// - /// path of the key in the Lambda event - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder WithEventKeyJmesPath(string eventKeyJmesPath) - { - _eventKeyJmesPath = eventKeyJmesPath; - return this; - } - - /// - /// Whether to locally cache idempotency results, by default false - /// - /// Indicate if a local cache must be used in addition to the persistence store. - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder WithUseLocalCache(bool useLocalCache) - { - _useLocalCache = useLocalCache; - return this; - } - - /// - /// A JMESPath expression to extract the payload to be validated from the event record. - /// See https://jmespath.org/ for more details. - /// - /// JMES Path of a part of the payload to be used for validation - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder WithPayloadValidationJmesPath(string payloadValidationJmesPath) - { - _payloadValidationJmesPath = payloadValidationJmesPath; - return this; - } - - /// - /// Whether to throw an exception if no idempotency key was found in the request, by default false - /// - /// boolean to indicate if we must throw an Exception when - /// idempotency key could not be found in the payload. - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdempotencyKey) - { - _throwOnNoIdempotencyKey = throwOnNoIdempotencyKey; - return this; - } - - /// - /// The number of seconds to wait before a record is expired - /// - /// expiration of the record in the store - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration) - { - _expirationInSeconds = (long) duration.TotalSeconds; - return this; - } - - /// - /// Function to use for calculating hashes, by default MD5. - /// - /// Can be any algorithm supported by HashAlgorithm.Create - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder WithHashFunction(string hashFunction) - { - _hashFunction = hashFunction; - return this; - } - - /// - /// Logs to a custom logger. - /// - /// The logger. - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder LogTo(ILog log) - { - _log = log; - return this; - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs new file mode 100644 index 00000000..95de4904 --- /dev/null +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs @@ -0,0 +1,117 @@ +using System; +using AWS.Lambda.Powertools.Idempotency.Output; + +namespace AWS.Lambda.Powertools.Idempotency; + +/// +/// Create a builder that can be used to configure and create +/// +public class IdempotencyOptionsBuilder +{ + private int _localCacheMaxItems = 256; + private bool _useLocalCache = false; + private long _expirationInSeconds = 60 * 60; // 1 hour + private string? _eventKeyJmesPath = null; + private string? _payloadValidationJmesPath = null; + private bool _throwOnNoIdempotencyKey = false; + private string _hashFunction = "MD5"; + private ILog _log = new NullLog(); + + /// + /// Initialize and return an instance of IdempotencyConfig. + /// Example: + /// IdempotencyConfig.Builder().WithUseLocalCache().Build(); + /// This instance must then be passed to the Idempotency.Config: + /// Idempotency.Config().WithConfig(config).Configure(); + /// + /// an instance of IdempotencyConfig + public IdempotencyOptions Build() => + new IdempotencyOptions(_eventKeyJmesPath, + _payloadValidationJmesPath, + _throwOnNoIdempotencyKey, + _useLocalCache, + _localCacheMaxItems, + _expirationInSeconds, + _hashFunction, + _log); + + /// + /// A JMESPath expression to extract the idempotency key from the event record. + /// See https://jmespath.org/ for more details. + /// + /// path of the key in the Lambda event + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithEventKeyJmesPath(string eventKeyJmesPath) + { + _eventKeyJmesPath = eventKeyJmesPath; + return this; + } + + /// + /// Whether to locally cache idempotency results, by default false + /// + /// Indicate if a local cache must be used in addition to the persistence store. + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithUseLocalCache(bool useLocalCache) + { + _useLocalCache = useLocalCache; + return this; + } + + /// + /// A JMESPath expression to extract the payload to be validated from the event record. + /// See https://jmespath.org/ for more details. + /// + /// JMES Path of a part of the payload to be used for validation + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithPayloadValidationJmesPath(string payloadValidationJmesPath) + { + _payloadValidationJmesPath = payloadValidationJmesPath; + return this; + } + + /// + /// Whether to throw an exception if no idempotency key was found in the request, by default false + /// + /// boolean to indicate if we must throw an Exception when + /// idempotency key could not be found in the payload. + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithThrowOnNoIdempotencyKey(bool throwOnNoIdempotencyKey) + { + _throwOnNoIdempotencyKey = throwOnNoIdempotencyKey; + return this; + } + + /// + /// The number of seconds to wait before a record is expired + /// + /// expiration of the record in the store + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithExpiration(TimeSpan duration) + { + _expirationInSeconds = (long) duration.TotalSeconds; + return this; + } + + /// + /// Function to use for calculating hashes, by default MD5. + /// + /// Can be any algorithm supported by HashAlgorithm.Create + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder WithHashFunction(string hashFunction) + { + _hashFunction = hashFunction; + return this; + } + + /// + /// Logs to a custom logger. + /// + /// The logger. + /// the instance of the builder (to chain operations) + public IdempotencyOptionsBuilder LogTo(ILog log) + { + _log = log; + return this; + } +} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs index 85ee2182..643b2dea 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -166,7 +166,11 @@ private Task HandleForStatus(DataRecord record) try { _log.WriteDebug("Response for key '{0}' retrieved from idempotency store, skipping the function", record.IdempotencyKey); - var result = JsonConvert.DeserializeObject(record.ResponseData); + T? result = JsonConvert.DeserializeObject(record.ResponseData!); + if (result is null) + { + throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name); + } return Task.FromResult(result); } catch (Exception e) @@ -206,7 +210,7 @@ private async Task GetFunctionResponse() try { - await _persistenceStore.SaveSuccess(_data, response, DateTimeOffset.UtcNow); + await _persistenceStore.SaveSuccess(_data, response!, DateTimeOffset.UtcNow); } catch (Exception e) { diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 0d31190b..56681eb0 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -287,7 +287,7 @@ private string GetHashedIdempotencyKey(JToken data) { throw new IdempotencyKeyException("No data found to create a hashed idempotency key"); } - Log.WriteWarning("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyOptions.EventKeyJmesPath); + Log.WriteWarning("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyOptions.EventKeyJmesPath ?? string.Empty); } string hash = GenerateHash(node); From b86e3f1aae7b1429b2498a2f2dfffe66dc4d93a3 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Thu, 6 Oct 2022 10:52:17 +0400 Subject: [PATCH 12/32] Use Instance as property --- .../Idempotency.cs | 24 +++++++++++++------ .../Internal/IdempotencyHandler.cs | 6 ++--- .../Persistence/BasePersistenceStoreTests.cs | 2 +- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 83ec2324..02a0572b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -25,7 +25,7 @@ namespace AWS.Lambda.Powertools.Idempotency; /// Use it before the function handler get called. /// Example: Idempotency.Configure(builder => builder.WithPersistenceStore(...)); /// -public class Idempotency +public sealed class Idempotency { /// /// The general configurations for the idempotency @@ -51,11 +51,21 @@ private void SetPersistenceStore(BasePersistenceStore persistenceStore) PersistenceStore = persistenceStore; } - private static class Holder { - public static readonly Idempotency IdempotencyInstance = new Idempotency(); + private class Holder + { + // Explicit static constructor to tell C# compiler + // not to mark type as beforefieldinit + static Holder() + { + } + + internal static readonly Idempotency IdempotencyInstance = new Idempotency(); } - public static Idempotency Instance() => Holder.IdempotencyInstance; + /// + /// Holds the configuration for idempotency: + /// + public static Idempotency Instance => Holder.IdempotencyInstance; /// /// Use this method to configure persistence layer (mandatory) and idempotency options (optional) @@ -71,13 +81,13 @@ public static void Configure(Action configurationAction) } if (builder.Options != null) { - Instance().SetConfig(builder.Options); + Instance.SetConfig(builder.Options); } else { - Instance().SetConfig(new IdempotencyOptionsBuilder().Build()); + Instance.SetConfig(new IdempotencyOptionsBuilder().Build()); } - Instance().SetPersistenceStore(builder.Store); + Instance.SetPersistenceStore(builder.Store); } /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs index 643b2dea..1bdda89f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -42,9 +42,9 @@ public IdempotencyHandler( _target = target; _args = args; _data = payload; - _persistenceStore = Idempotency.Instance().PersistenceStore; - _persistenceStore.Configure(Idempotency.Instance().IdempotencyOptions, functionName); - _log = Idempotency.Instance().IdempotencyOptions.Log; + _persistenceStore = Idempotency.Instance.PersistenceStore; + _persistenceStore.Configure(Idempotency.Instance.IdempotencyOptions, functionName); + _log = Idempotency.Instance.IdempotencyOptions.Log; } /// diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 41beec18..66068233 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -33,7 +33,7 @@ public class BasePersistenceStoreTests class InMemoryPersistenceStore : BasePersistenceStore { private string _validationHash = null; - public DataRecord? DataRecord = null; + public DataRecord DataRecord = null; public int Status = -1; public override Task GetRecord(string idempotencyKey) { From 432b65fd945ec3b5f620d7c757f28d997f4a4d35 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Thu, 6 Oct 2022 11:18:35 +0400 Subject: [PATCH 13/32] revert removed comment --- .../Core/PowertoolsLambdaContext.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs index efe7c1a3..b07013a8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs @@ -50,6 +50,9 @@ internal class PowertoolsLambdaContext /// internal int MemoryLimitInMB { get; private set; } + /// + /// The instance + /// internal static PowertoolsLambdaContext Instance { get; private set; } /// From 8b0395f98f4414b26c0d6afa17c7c33901ef0e01 Mon Sep 17 00:00:00 2001 From: Hossam Barakat Date: Fri, 7 Oct 2022 09:35:29 +0400 Subject: [PATCH 14/32] Fix minor issues in documentation --- docs/core/idempotency.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/core/idempotency.md b/docs/core/idempotency.md index ed49fa6b..4770ed1d 100644 --- a/docs/core/idempotency.md +++ b/docs/core/idempotency.md @@ -162,7 +162,7 @@ This persistence store is built-in, and you can either use an existing DynamoDB Use the builder to customize the table structure: ```csharp title="Customizing DynamoDBPersistenceStore to suit your table structure" new DynamoDBPersistenceStoreBuilder() - .WithTableName(System.getenv("TABLE_NAME")) + .WithTableName("TABLE_NAME") .WithKeyAttr("idempotency_key") .WithExpiryAttr("expires_at") .WithStatusAttr("current_status") @@ -188,7 +188,7 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by ### Customizing the default behavior -Idempotency behavior can be further configured with **`IdempotencyConfig`** using a builder: +Idempotency behavior can be further configured with **`IdempotencyOptions`** using a builder: ```csharp new IdempotencyOptionsBuilder() From 526310f22aa203d918bf3101780ce308d5f47cd9 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Fri, 5 May 2023 19:06:37 +0100 Subject: [PATCH 15/32] Remove references to Newtonsoft, remove testcontainers and persistencestoretests. fix tests --- .../IdempotentAttribute.cs | 4 +- .../Internal/IdempotencyHandler.cs | 11 +- .../Persistence/BasePersistenceStore.cs | 93 +-- ...Lambda.Powertools.Idempotency.Tests.csproj | 3 +- .../Handlers/IdempotencyFunction.cs | 13 +- .../IdempotencyTest.cs | 20 +- .../IntegrationTestBase.cs | 45 +- .../Internal/IdempotentAspectTests.cs | 22 +- .../Persistence/BasePersistenceStoreTests.cs | 78 +- .../DynamoDBPersistenceStoreTests.cs | 718 +++++++++--------- 10 files changed, 514 insertions(+), 493 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 4351bc34..9bb2c19d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -14,12 +14,12 @@ */ using System; +using System.Text.Json; using System.Threading.Tasks; using AspectInjector.Broker; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; -using Newtonsoft.Json.Linq; namespace AWS.Lambda.Powertools.Idempotency; @@ -89,7 +89,7 @@ protected sealed override async Task WrapAsync( { return await base.WrapAsync(target, args, eventArgs); } - JToken payload = JToken.FromObject(args[0]); + var payload = JsonSerializer.SerializeToDocument(args[0]); if (payload == null) { throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs index 1bdda89f..6599b574 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs @@ -14,12 +14,13 @@ */ using System; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Output; using AWS.Lambda.Powertools.Idempotency.Persistence; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; + namespace AWS.Lambda.Powertools.Idempotency.Internal; @@ -29,7 +30,7 @@ internal class IdempotencyHandler private readonly Func _target; private readonly object[] _args; - private readonly JToken _data; + private readonly JsonDocument _data; private readonly BasePersistenceStore _persistenceStore; private readonly ILog _log; @@ -37,7 +38,7 @@ public IdempotencyHandler( Func target, object[] args, string functionName, - JToken payload) + JsonDocument payload) { _target = target; _args = args; @@ -166,7 +167,7 @@ private Task HandleForStatus(DataRecord record) try { _log.WriteDebug("Response for key '{0}' retrieved from idempotency store, skipping the function", record.IdempotencyKey); - T? result = JsonConvert.DeserializeObject(record.ResponseData!); + T? result = JsonSerializer.Deserialize(record.ResponseData!); if (result is null) { throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 56681eb0..d17443a6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -16,14 +16,14 @@ using System; using System.Security.Cryptography; using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; using AWS.Lambda.Powertools.Idempotency.Output; using AWS.Lambda.Powertools.Idempotency.Serialization; using DevLab.JmesPath; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace AWS.Lambda.Powertools.Idempotency.Persistence; @@ -90,9 +90,9 @@ internal void Configure(IdempotencyOptions options, string functionName, LRUCach /// Payload /// the response from the function /// The current date time - public virtual async Task SaveSuccess(JToken data, object result, DateTimeOffset now) + public virtual async Task SaveSuccess(JsonDocument data, object result, DateTimeOffset now) { - string responseJson = JsonConvert.SerializeObject(result); + string responseJson = JsonSerializer.Serialize(result); var record = new DataRecord( GetHashedIdempotencyKey(data), DataRecord.DataRecordStatus.COMPLETED, @@ -111,7 +111,7 @@ public virtual async Task SaveSuccess(JToken data, object result, DateTimeOffset /// Payload /// The current date time /// - public virtual async Task SaveInProgress(JToken data, DateTimeOffset now) + public virtual async Task SaveInProgress(JsonDocument data, DateTimeOffset now) { string idempotencyKey = GetHashedIdempotencyKey(data); @@ -136,7 +136,7 @@ public virtual async Task SaveInProgress(JToken data, DateTimeOffset now) /// /// Payload /// The throwable thrown by the function - public virtual async Task DeleteRecord(JToken data, Exception throwable) + public virtual async Task DeleteRecord(JsonDocument data, Exception throwable) { string idemPotencyKey = GetHashedIdempotencyKey(data); @@ -155,7 +155,7 @@ public virtual async Task DeleteRecord(JToken data, Exception throwable) /// Payload /// /// DataRecord representation of existing record found in persistence store - public virtual async Task GetRecord(JToken data, DateTimeOffset now) + public virtual async Task GetRecord(JsonDocument data, DateTimeOffset now) { string idempotencyKey = GetHashedIdempotencyKey(data); @@ -195,7 +195,7 @@ private void SaveToCache(DataRecord dataRecord) /// Payload /// DataRecord instance /// - private void ValidatePayload(JToken data, DataRecord dataRecord) + private void ValidatePayload(JsonDocument data, DataRecord dataRecord) { if (PayloadValidationEnabled) { @@ -237,7 +237,7 @@ private void DeleteFromCache(string idempotencyKey) /// /// Payload /// Hashed representation of the data extracted by the jmespath expression - private string GetHashedPayload(JToken data) + private string GetHashedPayload(JsonDocument data) { if (!PayloadValidationEnabled) { @@ -246,9 +246,9 @@ private string GetHashedPayload(JToken data) var jmes = new JmesPath(); jmes.FunctionRepository.Register(); - var result = jmes.Transform(data.ToString(), _idempotencyOptions.PayloadValidationJmesPath); - var node = JToken.Parse(result); - return GenerateHash(node); + var result = jmes.Transform(data.RootElement.ToString(), _idempotencyOptions.PayloadValidationJmesPath); + var node = JsonDocument.Parse(result); + return GenerateHash(node.RootElement); } @@ -269,16 +269,16 @@ private long GetExpiryEpochSecond(DateTimeOffset now) /// incoming data /// Hashed representation of the data extracted by the jmespath expression /// - private string GetHashedIdempotencyKey(JToken data) + private string GetHashedIdempotencyKey(JsonDocument data) { - JToken node = data; + var node = data.RootElement; var eventKeyJmesPath = _idempotencyOptions.EventKeyJmesPath; if (eventKeyJmesPath != null) { var jmes = new JmesPath(); jmes.FunctionRepository.Register(); - var result = jmes.Transform(data.ToString(), eventKeyJmesPath); - node = JToken.Parse(result); + var result = jmes.Transform(node.ToString(), eventKeyJmesPath); + node = JsonDocument.Parse(result).RootElement; } if (IsMissingIdemPotencyKey(node)) @@ -294,13 +294,14 @@ private string GetHashedIdempotencyKey(JToken data) return _functionName + "#" + hash; } - private bool IsMissingIdemPotencyKey(JToken data) + private bool IsMissingIdemPotencyKey(JsonElement data) { - return (data == null) || - (data.Type == JTokenType.Array && !data.HasValues) || - (data.Type == JTokenType.Object && !data.HasValues) || - (data.Type == JTokenType.String && data.ToString() == String.Empty) || - (data.Type == JTokenType.Null); + return data.ValueKind == JsonValueKind.Null || data.ValueKind == JsonValueKind.Undefined; + // return (data == null) || + // (data is JsonArray && !data.) || + // (data.ValueKind == JsonValueKind.Array && !data.get) || + // (data.Type == JsonNodeType.String && data.ToString() == String.Empty) || + // (data.Type == JsonNodeType.Null); } /// @@ -309,30 +310,30 @@ private bool IsMissingIdemPotencyKey(JToken data) /// data to hash /// Hashed representation of the provided data /// - internal string GenerateHash(JToken data) + internal string GenerateHash(JsonElement data) { - JToken node; - if (data is JObject or JArray) // if array or object, use the json string representation, otherwise get the real value - { - node = data; - } - else if (data.Type == JTokenType.String) - { - node = data.Value(); - } - else if (data.Type == JTokenType.Integer) - { - node = data.Value(); - } - else if (data.Type == JTokenType.Float) - { - node = data.Value(); - } - else if (data.Type == JTokenType.Boolean) - { - node = data.Value(); - } - else node = data; // anything else + var node = data; + // if (data is JObject or JArray) // if array or object, use the json string representation, otherwise get the real value + // { + // node = data; + // } + // else if (data.Type == JsonNodeType.String) + // { + // node = data.Value(); + // } + // else if (data.Type == JsonNodeType.Integer) + // { + // node = data.Value(); + // } + // else if (data.Type == JsonNodeType.Float) + // { + // node = data.Value(); + // } + // else if (data.Type == JsonNodeType.Boolean) + // { + // node = data.Value(); + // } + // else node = data; // anything else using var hashAlgorithm = HashAlgorithm.Create(_idempotencyOptions.HashFunction); @@ -340,7 +341,7 @@ internal string GenerateHash(JToken data) { throw new ArgumentException("Invalid HashAlgorithm"); } - var stringToHash = node is JValue ? node.ToString() : node.ToString(Formatting.None); + var stringToHash = node.ToString(); string hash = GetHash(hashAlgorithm, stringToHash); return hash; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj index 249b81e6..8d68afb8 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj @@ -9,7 +9,6 @@ - @@ -37,7 +36,7 @@ - PreserveNewest + Always diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs index f296b5b8..76242121 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs @@ -13,15 +13,17 @@ * permissions and limitations under the License. */ +using System; using System.Collections.Generic; using System.IO; using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Amazon; using Amazon.DynamoDBv2; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; -using Newtonsoft.Json.Linq; namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; @@ -34,15 +36,14 @@ public IdempotencyFunction(AmazonDynamoDBClient client) Idempotency.Configure(builder => builder .WithOptions(optionsBuilder => - optionsBuilder.WithEventKeyJmesPath("powertools_json(Body).address")) + optionsBuilder + .WithEventKeyJmesPath("powertools_json(Body).address") + .WithExpiration(TimeSpan.FromSeconds(20))) .UseDynamoDb(storeBuilder => storeBuilder .WithTableName("idempotency_table") .WithDynamoDBClient(client) )); - - - } [Idempotent] @@ -65,7 +66,7 @@ private async Task InternalFunctionHandler(APIGatewayPr try { - string address = JToken.Parse(apigProxyEvent.Body)["address"].Value(); + string address = JsonDocument.Parse(apigProxyEvent.Body).RootElement.GetProperty("address").GetString(); string pageContents = await GetPageContents(address); string output = $"{{ \"message\": \"hello world\", \"location\": \"{pageContents}\" }}"; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs index cb2b2913..6e1ea231 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -13,30 +13,40 @@ * permissions and limitations under the License. */ +using System; using System.IO; +using System.Text.Json; using System.Threading.Tasks; +using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Idempotency.Tests.Handlers; using FluentAssertions; -using Newtonsoft.Json; using Xunit; namespace AWS.Lambda.Powertools.Idempotency.Tests; -public class IdempotencyTest : IntegrationTestBase +public class IdempotencyTest { + protected const string TABLE_NAME = "idempotency_table"; + [Fact] public async Task EndToEndTest() { + var client = new AmazonDynamoDBClient(); + IdempotencyFunction function = new IdempotencyFunction(client); + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + //var persistenceStore = new InMemoryPersistenceStore(); TestLambdaContext context = new TestLambdaContext(); - var request = JsonConvert.DeserializeObject(File.ReadAllText("./resources/apigw_event2.json")); + var request = JsonSerializer.Deserialize(File.ReadAllText("./resources/apigw_event2.json"),options); - APIGatewayProxyResponse response = await function.Handle(request, context); function.HandlerExecuted.Should().BeTrue(); @@ -45,7 +55,7 @@ public async Task EndToEndTest() var response2 = await function.Handle(request, context); function.HandlerExecuted.Should().BeFalse(); - JsonConvert.SerializeObject(response).Should().Be(JsonConvert.SerializeObject(response)); + JsonSerializer.Serialize(response).Should().Be(JsonSerializer.Serialize(response)); response2.Body.Should().Contain("hello world"); var scanResponse = await client.ScanAsync(new ScanRequest diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs index 918d800a..3661977e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs @@ -19,30 +19,23 @@ using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; +using AWS.Lambda.Powertools.Idempotency.Persistence; using Xunit; namespace AWS.Lambda.Powertools.Idempotency.Tests; -public class IntegrationTestBase: IAsyncLifetime +public class IntegrationTestBase : IAsyncLifetime { protected const string TABLE_NAME = "idempotency_table"; protected AmazonDynamoDBClient client; - private TestcontainersContainer _testContainer; + private protected DynamoDBPersistenceStore _dynamoDbPersistenceStore; - public virtual async Task InitializeAsync() + public async Task InitializeAsync() { - _testContainer = new TestcontainersBuilder() - .WithImage("amazon/dynamodb-local:latest") - .WithPortBinding(8000, assignRandomHostPort:true) - .WithDockerEndpoint(Environment.GetEnvironmentVariable("DOCKER_HOST") ?? "unix:///var/run/docker.sock") - .Build(); - await _testContainer.StartAsync(); var credentials = new BasicAWSCredentials("FAKE", "FAKE"); var amazonDynamoDbConfig = new AmazonDynamoDBConfig() { - ServiceURL = $"http://localhost:{_testContainer.GetMappedPublicPort(8000)}", + ServiceURL = new UriBuilder("http", "localhost", 8000).Uri.ToString(), AuthenticationRegion = "us-east-1" }; client = new AmazonDynamoDBClient(credentials, amazonDynamoDbConfig); @@ -64,16 +57,32 @@ public virtual async Task InitializeAsync() }, BillingMode = BillingMode.PAY_PER_REQUEST }; - await client.CreateTableAsync(createTableRequest); - var response = await client.DescribeTableAsync(TABLE_NAME); - if (response == null) + try { - throw new NullReferenceException("Table was not created within the expected time"); + await client.CreateTableAsync(createTableRequest); + var response = await client.DescribeTableAsync(TABLE_NAME); + if (response == null) + { + throw new NullReferenceException("Table was not created within the expected time"); + } } + catch (ResourceInUseException e) + { + Console.WriteLine(e.Message); + } + + _dynamoDbPersistenceStore = new DynamoDBPersistenceStoreBuilder() + .WithTableName(TABLE_NAME) + .WithDynamoDBClient(client) + .Build(); + _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); } - public virtual Task DisposeAsync() + public Task DisposeAsync() { - return _testContainer.DisposeAsync().AsTask(); + // Make sure delete item after each test + _dynamoDbPersistenceStore.DeleteRecord("key").ConfigureAwait(false); + //_dynamoContainer.DisposeAsync().ConfigureAwait(false); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index 4baa2e0d..e7293fc4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -14,7 +14,8 @@ */ using System; -using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Idempotency.Exceptions; @@ -24,7 +25,6 @@ using AWS.Lambda.Powertools.Idempotency.Tests.Model; using FluentAssertions; using Moq; -using Newtonsoft.Json.Linq; using Xunit; namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; @@ -53,10 +53,10 @@ public async Task Handle_WhenFirstCall_ShouldPutInStore() function.HandlerExecuted.Should().BeTrue(); store - .Verify(x=>x.SaveInProgress(It.Is(t=> t.ToString() == JToken.FromObject(product).ToString()), It.IsAny())); + .Verify(x=>x.SaveInProgress(It.Is(t=> t.ToString() == JsonSerializer.SerializeToDocument(product, It.IsAny()).ToString()), It.IsAny())); store - .Verify(x=>x.SaveSuccess(It.IsAny(), It.Is(y => y.Equals(basket)), It.IsAny())); + .Verify(x=>x.SaveSuccess(It.IsAny(), It.Is(y => y.Equals(basket)), It.IsAny())); } [Fact] @@ -64,7 +64,7 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() { //Arrange var store = new Mock(); - store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) + store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) .Throws(); // GIVEN @@ -80,9 +80,9 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() "42", DataRecord.DataRecordStatus.COMPLETED, DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), - JToken.FromObject(basket).ToString(), + JsonSerializer.SerializeToNode(basket)!.ToString(), null); - store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) + store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); @@ -106,7 +106,7 @@ public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempoten .WithPersistenceStore(store.Object) .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) + store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) .Throws(); Product product = new Product(42, "fake product", 12); @@ -115,9 +115,9 @@ public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempoten "42", DataRecord.DataRecordStatus.INPROGRESS, DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), - JToken.FromObject(basket).ToString(), + JsonSerializer.SerializeToNode(basket)!.ToString(), null); - store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) + store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); // Act @@ -149,7 +149,7 @@ public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionE // Assert await act.Should().ThrowAsync(); store.Verify( - x => x.DeleteRecord(It.IsAny(), It.IsAny())); + x => x.DeleteRecord(It.IsAny(), It.IsAny())); } [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 66068233..9dd912dc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -15,6 +15,8 @@ using System; using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Amazon.Lambda.APIGatewayEvents; using AWS.Lambda.Powertools.Idempotency.Exceptions; @@ -22,8 +24,6 @@ using AWS.Lambda.Powertools.Idempotency.Persistence; using AWS.Lambda.Powertools.Idempotency.Tests.Model; using FluentAssertions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Xunit; namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; @@ -81,14 +81,14 @@ public async Task SaveInProgress_WhenDefaultConfig_ShouldSaveRecordInStore() DateTimeOffset now = DateTimeOffset.UtcNow; // Act - await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert var dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); dr.ResponseData.Should().BeNull(); - dr.IdempotencyKey.Should().Be("testFunction#36e3de9a3270f82fb957c645178dfab9"); + dr.IdempotencyKey.Should().Be("testFunction#b105f675a45bab746c0723da594d3b06"); dr.PayloadHash.Should().BeEmpty(); persistenceStore.Status.Should().Be(1); } @@ -107,7 +107,7 @@ public async Task SaveInProgress_WhenKeyJmesPathIsSet_ShouldSaveRecordInStore_Wi DateTimeOffset now = DateTimeOffset.UtcNow; // Act - await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert var dr = persistenceStore.DataRecord; @@ -134,7 +134,7 @@ public async Task SaveInProgress_WhenJMESPath_NotFound_ShouldThrowException() DateTimeOffset now = DateTimeOffset.UtcNow; // Act - Func act = async () => await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + Func act = async () => await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert await act.Should() @@ -158,7 +158,7 @@ public async Task SaveInProgress_WhenJMESpath_NotFound_ShouldNotThrowException() DateTimeOffset now = DateTimeOffset.UtcNow; // Act - await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert DataRecord dr = persistenceStore.DataRecord; @@ -189,7 +189,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSet_AndNotExpired_ShouldThrowEx ); // Act - Func act = () => persistenceStore.SaveInProgress(JToken.FromObject(request), now); + Func act = () => persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert await act.Should() @@ -222,7 +222,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSetButExpired_ShouldRemoveFromC ); // Act - await persistenceStore.SaveInProgress(JToken.FromObject(request), now); + await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert DataRecord dr = persistenceStore.DataRecord; @@ -247,14 +247,14 @@ public async Task SaveSuccess_WhenDefaultConfig_ShouldUpdateRecord() DateTimeOffset now = DateTimeOffset.UtcNow; // Act - await persistenceStore.SaveSuccess(JToken.FromObject(request), product, now); + await persistenceStore.SaveSuccess(JsonSerializer.SerializeToDocument(request)!, product, now); // Assert DataRecord dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); - dr.ResponseData.Should().Be(JsonConvert.SerializeObject(product)); - dr.IdempotencyKey.Should().Be("testFunction#36e3de9a3270f82fb957c645178dfab9"); + dr.ResponseData.Should().Be(JsonSerializer.Serialize(product)); + dr.IdempotencyKey.Should().Be("testFunction#b105f675a45bab746c0723da594d3b06"); dr.PayloadHash.Should().BeEmpty(); persistenceStore.Status.Should().Be(2); cache.Count.Should().Be(0); @@ -275,18 +275,18 @@ public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() DateTimeOffset now = DateTimeOffset.UtcNow; // Act - await persistenceStore.SaveSuccess(JToken.FromObject(request), product, now); + await persistenceStore.SaveSuccess(JsonSerializer.SerializeToDocument(request)!, product, now); // Assert persistenceStore.Status.Should().Be(2); cache.Count.Should().Be(1); - var foundDataRecord = cache.TryGet("testFunction#36e3de9a3270f82fb957c645178dfab9", out DataRecord record); + var foundDataRecord = cache.TryGet("testFunction#b105f675a45bab746c0723da594d3b06", out DataRecord record); foundDataRecord.Should().BeTrue(); record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); record.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); - record.ResponseData.Should().Be(JsonConvert.SerializeObject(product)); - record.IdempotencyKey.Should().Be("testFunction#36e3de9a3270f82fb957c645178dfab9"); + record.ResponseData.Should().Be(JsonSerializer.Serialize(product)); + record.IdempotencyKey.Should().Be("testFunction#b105f675a45bab746c0723da594d3b06"); record.PayloadHash.Should().BeEmpty(); } @@ -305,10 +305,10 @@ public async Task GetRecord_WhenRecordIsInStore_ShouldReturnRecordFromPersistenc DateTimeOffset now = DateTimeOffset.UtcNow; // Act - DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + DataRecord record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert - record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); + record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); record.ResponseData.Should().Be("Response"); persistenceStore.Status.Should().Be(0); @@ -327,18 +327,18 @@ public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCac DateTimeOffset now = DateTimeOffset.UtcNow; DataRecord dr = new DataRecord( - "testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", + "testFunction.myfunc#b105f675a45bab746c0723da594d3b06", DataRecord.DataRecordStatus.COMPLETED, now.AddSeconds(3600).ToUnixTimeSeconds(), "result of the function", null); - cache.Set("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", dr); + cache.Set("testFunction.myfunc#b105f675a45bab746c0723da594d3b06", dr); // Act - DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + DataRecord record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert - record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); + record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); record.ResponseData.Should().Be("result of the function"); persistenceStore.Status.Should().Be(-1); @@ -356,18 +356,18 @@ public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRe DateTimeOffset now = DateTimeOffset.UtcNow; DataRecord dr = new DataRecord( - "testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", + "testFunction.myfunc#b105f675a45bab746c0723da594d3b06", DataRecord.DataRecordStatus.COMPLETED, now.AddSeconds(-3).ToUnixTimeSeconds(), "result of the function", null); - cache.Set("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9", dr); + cache.Set("testFunction.myfunc#b105f675a45bab746c0723da594d3b06", dr); // Act - DataRecord record = await persistenceStore.GetRecord(JToken.FromObject(request), now); + DataRecord record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert - record.IdempotencyKey.Should().Be("testFunction.myfunc#36e3de9a3270f82fb957c645178dfab9"); + record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); record.ResponseData.Should().Be("Response"); persistenceStore.Status.Should().Be(0); @@ -390,7 +390,7 @@ public async Task GetRecord_WhenInvalidPayload_ShouldThrowValidationException() DateTimeOffset now = DateTimeOffset.UtcNow; // Act - Func act = () => persistenceStore.GetRecord(JToken.FromObject(request), now); + Func act = () => persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert await act.Should().ThrowAsync(); @@ -407,7 +407,7 @@ public async Task DeleteRecord_WhenRecordExist_ShouldDeleteRecordFromPersistence persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); // Act - await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); + await persistenceStore.DeleteRecord(JsonSerializer.SerializeToDocument(request)!, new ArithmeticException()); // Assert persistenceStore.Status.Should().Be(3); @@ -423,14 +423,14 @@ public async Task DeleteRecord_WhenLocalCacheEnabled_ShouldDeleteRecordFromCache persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), null, cache); - cache.Set("testFunction#36e3de9a3270f82fb957c645178dfab9", - new DataRecord("testFunction#36e3de9a3270f82fb957c645178dfab9", + cache.Set("testFunction#b105f675a45bab746c0723da594d3b06", + new DataRecord("testFunction#b105f675a45bab746c0723da594d3b06", DataRecord.DataRecordStatus.COMPLETED, 123, null, null)); // Act - await persistenceStore.DeleteRecord(JToken.FromObject(request), new ArithmeticException()); + await persistenceStore.DeleteRecord(JsonSerializer.SerializeToDocument(request)!, new ArithmeticException()); // Assert persistenceStore.Status.Should().Be(3); @@ -446,7 +446,8 @@ public void GenerateHash_WhenInputIsString_ShouldGenerateMd5ofString() string expectedHash = "70c24d88041893f7fbab4105b76fd9e1"; // MD5(Lambda rocks) // Act - string generatedHash = persistenceStore.GenerateHash(new JValue("Lambda rocks")); + var jsonValue = JsonValue.Create("Lambda rocks"); + string generatedHash = persistenceStore.GenerateHash( JsonDocument.Parse(jsonValue.ToJsonString()).RootElement); // Assert generatedHash.Should().Be(expectedHash); @@ -459,10 +460,10 @@ public void GenerateHash_WhenInputIsObject_ShouldGenerateMd5ofJsonObject() var persistenceStore = new InMemoryPersistenceStore(); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); Product product = new Product(42, "Product", 12); - string expectedHash = "87dd2e12074c65c9bac728795a6ebb45"; // MD5({"Id":42,"Name":"Product","Price":12.0}) + string expectedHash = "c83e720b399b3b4898c8734af177c53a"; // MD5({"Id":42,"Name":"Product","Price":12}) // Act - string generatedHash = persistenceStore.GenerateHash(JToken.FromObject(product)); + string generatedHash = persistenceStore.GenerateHash(JsonSerializer.SerializeToDocument(product)!.RootElement); // Assert generatedHash.Should().Be(expectedHash); @@ -477,7 +478,7 @@ public void GenerateHash_WhenInputIsDouble_ShouldGenerateMd5ofDouble() string expectedHash = "bb84c94278119c8838649706df4db42b"; // MD5(256.42) // Act - var generatedHash = persistenceStore.GenerateHash(new JValue(256.42)); + var generatedHash = persistenceStore.GenerateHash(JsonDocument.Parse("256.42").RootElement); // Assert generatedHash.Should().Be(expectedHash); @@ -485,8 +486,13 @@ public void GenerateHash_WhenInputIsDouble_ShouldGenerateMd5ofDouble() private static APIGatewayProxyRequest LoadApiGatewayProxyRequest() { + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + var eventJson = File.ReadAllText("./resources/apigw_event.json"); - var request = JsonConvert.DeserializeObject(eventJson); + var request = JsonSerializer.Deserialize(eventJson, options); return request!; } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index da6efd0b..8f142b18 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -1,362 +1,356 @@ -/* - * 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.Collections; -using System.Collections.Generic; -using System.Threading.Tasks; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; -using AWS.Lambda.Powertools.Idempotency.Exceptions; -using AWS.Lambda.Powertools.Idempotency.Internal; -using AWS.Lambda.Powertools.Idempotency.Persistence; -using FluentAssertions; -using Xunit; - -namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; - -public class DynamoDBPersistenceStoreTests : IntegrationTestBase -{ - private DynamoDBPersistenceStore _dynamoDbPersistenceStore; - - public override async Task InitializeAsync() - { - await base.InitializeAsync(); - _dynamoDbPersistenceStore = new DynamoDBPersistenceStoreBuilder() - .WithTableName(TABLE_NAME) - .WithDynamoDBClient(client) - .Build(); - _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); - } - //putRecord - [Fact] - public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB() - { - // Arrange - DateTimeOffset now = DateTimeOffset.UtcNow; - long expiry = now.AddSeconds(3600).ToUnixTimeSeconds(); - var key = CreateKey("key"); - - // Act - await _dynamoDbPersistenceStore - .PutRecord(new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, null, null), now); - - // Assert - var getItemResponse = - await client.GetItemAsync(new GetItemRequest - { - TableName = TABLE_NAME, - Key = key - }); - - var item = getItemResponse.Item; - item.Should().NotBeNull(); - item["status"].S.Should().Be("COMPLETED"); - item["expiration"].N.Should().Be(expiry.ToString()); - } - - [Fact] - public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlreadyExistsException() - { - // Arrange - var key = CreateKey("key"); - - // Insert a fake item with same id - Dictionary item = new(key); - DateTimeOffset now = DateTimeOffset.UtcNow; - long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); - item.Add("expiration", new AttributeValue(){N = expiry.ToString()}); - item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); - item.Add("data", new AttributeValue("Fake Data")); - await client.PutItemAsync(new PutItemRequest - { - TableName = TABLE_NAME, - Item = item - }); - long expiry2 = now.AddSeconds(3600).ToUnixTimeSeconds(); - - // Act - Func act = () => _dynamoDbPersistenceStore.PutRecord( - new DataRecord("key", - DataRecord.DataRecordStatus.INPROGRESS, - expiry2, - null, - null - ), now); - - // Assert - await act.Should().ThrowAsync(); - - // item was not updated, retrieve the initial one - Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest - { - TableName = TABLE_NAME, - Key = key - })).Item; - itemInDb.Should().NotBeNull(); - itemInDb["status"].S.Should().Be("COMPLETED"); - itemInDb["expiration"].N.Should().Be(expiry.ToString()); - itemInDb["data"].S.Should().Be("Fake Data"); - } - - //getRecord - [Fact] - public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecord() - { - // Arrange - // Insert a fake item with same id - Dictionary item = new() - { - {"id", new AttributeValue("key")} //key - }; - DateTimeOffset now = DateTimeOffset.UtcNow; - long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); - item.Add("expiration", new AttributeValue - { - N = expiry.ToString() - }); - item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); - item.Add("data", new AttributeValue("Fake Data")); - var response = await client.PutItemAsync(new PutItemRequest() - { - TableName = TABLE_NAME, - Item = item - }); - - // Act - DataRecord record = await _dynamoDbPersistenceStore.GetRecord("key"); - - // Assert - record.IdempotencyKey.Should().Be("key"); - record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); - record.ResponseData.Should().Be("Fake Data"); - record.ExpiryTimestamp.Should().Be(expiry); - } - - [Fact] - public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() - { - // Act - Func act = () => _dynamoDbPersistenceStore.GetRecord("key"); - - // Assert - await act.Should().ThrowAsync(); - } - //updateRecord - - [Fact] - public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() - { - // Arrange: Insert a fake item with same id - var key = CreateKey("key"); - Dictionary item = new(key); - DateTimeOffset now = DateTimeOffset.UtcNow; - long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); - item.Add("expiration", new AttributeValue - { - N = expiry.ToString() - }); - item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); - await client.PutItemAsync(new PutItemRequest - { - TableName = TABLE_NAME, - Item = item - }); - // enable payload validation - _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().WithPayloadValidationJmesPath("path").Build(), - null); - - // Act - expiry = now.AddSeconds(3600).ToUnixTimeMilliseconds(); - DataRecord record = new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, "Fake result", "hash"); - await _dynamoDbPersistenceStore.UpdateRecord(record); - - // Assert - Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest - { - TableName = TABLE_NAME, - Key = key - })).Item; - - itemInDb["status"].S.Should().Be("COMPLETED"); - itemInDb["expiration"].N.Should().Be(expiry.ToString()); - itemInDb["data"].S.Should().Be("Fake result"); - itemInDb["validation"].S.Should().Be("hash"); - } - - //deleteRecord - [Fact] - public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() - { - // Arrange: Insert a fake item with same id - var key = CreateKey("key"); - Dictionary item = new(key); - DateTimeOffset now = DateTimeOffset.UtcNow; - long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); - item.Add("expiration", new AttributeValue(){N=expiry.ToString()}); - item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); - await client.PutItemAsync(new PutItemRequest - { - TableName = TABLE_NAME, - Item = item - }); - var scanResponse = await client.ScanAsync(new ScanRequest - { - TableName = TABLE_NAME - }); - scanResponse.Items.Count.Should().Be(1); - - // Act - await _dynamoDbPersistenceStore.DeleteRecord("key"); - - // Assert - scanResponse = await client.ScanAsync(new ScanRequest - { - TableName = TABLE_NAME - }); - scanResponse.Items.Count.Should().Be(0); - } - - [Fact] - public async Task EndToEndWithCustomAttrNamesAndSortKey() - { - var TABLE_NAME_CUSTOM = "idempotency_table_custom"; - try - { - var createTableRequest = new CreateTableRequest - { - TableName = TABLE_NAME_CUSTOM, - KeySchema = new List() - { - new KeySchemaElement("key", KeyType.HASH), - new KeySchemaElement("sortkey", KeyType.RANGE) - }, - AttributeDefinitions = new List() - { - new AttributeDefinition("key", ScalarAttributeType.S), - new AttributeDefinition("sortkey", ScalarAttributeType.S) - }, - BillingMode = BillingMode.PAY_PER_REQUEST - }; - await client.CreateTableAsync(createTableRequest); - DynamoDBPersistenceStore persistenceStore = new DynamoDBPersistenceStoreBuilder() - .WithTableName(TABLE_NAME_CUSTOM) - .WithDynamoDBClient(client) - .WithDataAttr("result") - .WithExpiryAttr("expiry") - .WithKeyAttr("key") - .WithSortKeyAttr("sortkey") - .WithStaticPkValue("pk") - .WithStatusAttr("state") - .WithValidationAttr("valid") - .Build(); - persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); - - DateTimeOffset now = DateTimeOffset.UtcNow; - DataRecord record = new DataRecord( - "mykey", - DataRecord.DataRecordStatus.INPROGRESS, - now.AddSeconds(400).ToUnixTimeMilliseconds(), - null, - null - ); - // PUT - await persistenceStore.PutRecord(record, now); - - Dictionary customKey = new(); - customKey.Add("key", new AttributeValue("pk")); - customKey.Add("sortkey", new AttributeValue("mykey")); - - Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest - { - TableName = TABLE_NAME_CUSTOM, - Key = customKey - })).Item; - - // GET - DataRecord recordInDb = await persistenceStore.GetRecord("mykey"); - - itemInDb.Should().NotBeNull(); - itemInDb["key"].S.Should().Be("pk"); - itemInDb["sortkey"].S.Should().Be(recordInDb.IdempotencyKey); - itemInDb["state"].S.Should().Be(recordInDb.Status.ToString()); - itemInDb["expiry"].N.Should().Be(recordInDb.ExpiryTimestamp.ToString()); - - // UPDATE - DataRecord updatedRecord = new DataRecord( - "mykey", - DataRecord.DataRecordStatus.COMPLETED, - now.AddSeconds(500).ToUnixTimeMilliseconds(), - "response", - null - ); - await persistenceStore.UpdateRecord(updatedRecord); - recordInDb = await persistenceStore.GetRecord("mykey"); - recordInDb.Should().Be(updatedRecord); - - // DELETE - await persistenceStore.DeleteRecord("mykey"); - (await client.ScanAsync(new ScanRequest - { - TableName = TABLE_NAME_CUSTOM - })).Count.Should().Be(0); - - } - finally - { - try - { - await client.DeleteTableAsync(new DeleteTableRequest - { - TableName = TABLE_NAME_CUSTOM - }); - } - catch (Exception) - { - // OK - } - } - } - - [Fact] - public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() - { - try - { - // Arrange - Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); - - DynamoDBPersistenceStore store = new DynamoDBPersistenceStoreBuilder().WithTableName(TABLE_NAME).Build(); - - // Act - Func act = () => store.GetRecord("fake"); - - // Assert - await act.Should().ThrowAsync(); - } - finally - { - Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); - } - } - private static Dictionary CreateKey(string keyValue) - { - var key = new Dictionary() - { - {"id", new AttributeValue(keyValue)} - }; - return key; - } -} \ No newline at end of file +// /* +// * 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.Collections; +// using System.Collections.Generic; +// using System.Threading.Tasks; +// using Amazon.DynamoDBv2; +// using Amazon.DynamoDBv2.Model; +// using AWS.Lambda.Powertools.Idempotency.Exceptions; +// using AWS.Lambda.Powertools.Idempotency.Internal; +// using AWS.Lambda.Powertools.Idempotency.Persistence; +// using FluentAssertions; +// using Xunit; +// +// [assembly: CollectionBehavior(DisableTestParallelization = true)] +// +// namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; +// +// [Collection("Sequential")] +// public class DynamoDBPersistenceStoreTests : IntegrationTestBase +// { +// //putRecord +// [Fact] +// public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB() +// { +// // Arrange +// DateTimeOffset now = DateTimeOffset.UtcNow; +// long expiry = now.AddSeconds(3600).ToUnixTimeSeconds(); +// var key = CreateKey("key"); +// +// // Act +// await _dynamoDbPersistenceStore +// .PutRecord(new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, null, null), now); +// +// // Assert +// var getItemResponse = +// await client.GetItemAsync(new GetItemRequest +// { +// TableName = TABLE_NAME, +// Key = key +// }); +// +// var item = getItemResponse.Item; +// item.Should().NotBeNull(); +// item["status"].S.Should().Be("COMPLETED"); +// item["expiration"].N.Should().Be(expiry.ToString()); +// } +// +// [Fact] +// public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlreadyExistsException() +// { +// // Arrange +// var key = CreateKey("key"); +// +// // Insert a fake item with same id +// Dictionary item = new(key); +// DateTimeOffset now = DateTimeOffset.UtcNow; +// long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); +// item.Add("expiration", new AttributeValue(){N = expiry.ToString()}); +// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); +// item.Add("data", new AttributeValue("Fake Data")); +// await client.PutItemAsync(new PutItemRequest +// { +// TableName = TABLE_NAME, +// Item = item +// }); +// long expiry2 = now.AddSeconds(3600).ToUnixTimeSeconds(); +// +// // Act +// Func act = () => _dynamoDbPersistenceStore.PutRecord( +// new DataRecord("key", +// DataRecord.DataRecordStatus.INPROGRESS, +// expiry2, +// null, +// null +// ), now); +// +// // Assert +// await act.Should().ThrowAsync(); +// +// // item was not updated, retrieve the initial one +// Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest +// { +// TableName = TABLE_NAME, +// Key = key +// })).Item; +// itemInDb.Should().NotBeNull(); +// itemInDb["status"].S.Should().Be("COMPLETED"); +// itemInDb["expiration"].N.Should().Be(expiry.ToString()); +// itemInDb["data"].S.Should().Be("Fake Data"); +// } +// +// //getRecord +// [Fact] +// public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecord() +// { +// // Arrange +// //await InitializeAsync(); +// +// // Insert a fake item with same id +// Dictionary item = new() +// { +// {"id", new AttributeValue("key")} //key +// }; +// DateTimeOffset now = DateTimeOffset.UtcNow; +// long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); +// item.Add("expiration", new AttributeValue +// { +// N = expiry.ToString() +// }); +// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); +// item.Add("data", new AttributeValue("Fake Data")); +// var response = await client.PutItemAsync(new PutItemRequest() +// { +// TableName = TABLE_NAME, +// Item = item +// }); +// +// // Act +// DataRecord record = await _dynamoDbPersistenceStore.GetRecord("key"); +// +// // Assert +// record.IdempotencyKey.Should().Be("key"); +// record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); +// record.ResponseData.Should().Be("Fake Data"); +// record.ExpiryTimestamp.Should().Be(expiry); +// } +// +// [Fact] +// public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() +// { +// // Act +// Func act = () => _dynamoDbPersistenceStore.GetRecord("key"); +// +// // Assert +// await act.Should().ThrowAsync(); +// } +// //updateRecord +// +// [Fact] +// public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() +// { +// // Arrange: Insert a fake item with same id +// var key = CreateKey("key"); +// Dictionary item = new(key); +// DateTimeOffset now = DateTimeOffset.UtcNow; +// long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); +// item.Add("expiration", new AttributeValue +// { +// N = expiry.ToString() +// }); +// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); +// await client.PutItemAsync(new PutItemRequest +// { +// TableName = TABLE_NAME, +// Item = item +// }); +// // enable payload validation +// _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().WithPayloadValidationJmesPath("path").Build(), +// null); +// +// // Act +// expiry = now.AddSeconds(3600).ToUnixTimeMilliseconds(); +// DataRecord record = new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, "Fake result", "hash"); +// await _dynamoDbPersistenceStore.UpdateRecord(record); +// +// // Assert +// Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest +// { +// TableName = TABLE_NAME, +// Key = key +// })).Item; +// +// itemInDb["status"].S.Should().Be("COMPLETED"); +// itemInDb["expiration"].N.Should().Be(expiry.ToString()); +// itemInDb["data"].S.Should().Be("Fake result"); +// itemInDb["validation"].S.Should().Be("hash"); +// } +// +// //deleteRecord +// [Fact] +// public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() +// { +// // Arrange: Insert a fake item with same id +// var key = CreateKey("key"); +// Dictionary item = new(key); +// DateTimeOffset now = DateTimeOffset.UtcNow; +// long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); +// item.Add("expiration", new AttributeValue(){N=expiry.ToString()}); +// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); +// await client.PutItemAsync(new PutItemRequest +// { +// TableName = TABLE_NAME, +// Item = item +// }); +// var scanResponse = await client.ScanAsync(new ScanRequest +// { +// TableName = TABLE_NAME +// }); +// scanResponse.Items.Count.Should().Be(1); +// +// // Act +// await _dynamoDbPersistenceStore.DeleteRecord("key"); +// +// // Assert +// scanResponse = await client.ScanAsync(new ScanRequest +// { +// TableName = TABLE_NAME +// }); +// scanResponse.Items.Count.Should().Be(0); +// } +// +// [Fact] +// public async Task EndToEndWithCustomAttrNamesAndSortKey() +// { +// var TABLE_NAME_CUSTOM = "idempotency_table_custom"; +// try +// { +// var createTableRequest = new CreateTableRequest +// { +// TableName = TABLE_NAME_CUSTOM, +// KeySchema = new List() +// { +// new KeySchemaElement("key", KeyType.HASH), +// new KeySchemaElement("sortkey", KeyType.RANGE) +// }, +// AttributeDefinitions = new List() +// { +// new AttributeDefinition("key", ScalarAttributeType.S), +// new AttributeDefinition("sortkey", ScalarAttributeType.S) +// }, +// BillingMode = BillingMode.PAY_PER_REQUEST +// }; +// await client.CreateTableAsync(createTableRequest); +// DynamoDBPersistenceStore persistenceStore = new DynamoDBPersistenceStoreBuilder() +// .WithTableName(TABLE_NAME_CUSTOM) +// .WithDynamoDBClient(client) +// .WithDataAttr("result") +// .WithExpiryAttr("expiry") +// .WithKeyAttr("key") +// .WithSortKeyAttr("sortkey") +// .WithStaticPkValue("pk") +// .WithStatusAttr("state") +// .WithValidationAttr("valid") +// .Build(); +// persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); +// +// DateTimeOffset now = DateTimeOffset.UtcNow; +// DataRecord record = new DataRecord( +// "mykey", +// DataRecord.DataRecordStatus.INPROGRESS, +// now.AddSeconds(400).ToUnixTimeMilliseconds(), +// null, +// null +// ); +// // PUT +// await persistenceStore.PutRecord(record, now); +// +// Dictionary customKey = new(); +// customKey.Add("key", new AttributeValue("pk")); +// customKey.Add("sortkey", new AttributeValue("mykey")); +// +// Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest +// { +// TableName = TABLE_NAME_CUSTOM, +// Key = customKey +// })).Item; +// +// // GET +// DataRecord recordInDb = await persistenceStore.GetRecord("mykey"); +// +// itemInDb.Should().NotBeNull(); +// itemInDb["key"].S.Should().Be("pk"); +// itemInDb["sortkey"].S.Should().Be(recordInDb.IdempotencyKey); +// itemInDb["state"].S.Should().Be(recordInDb.Status.ToString()); +// itemInDb["expiry"].N.Should().Be(recordInDb.ExpiryTimestamp.ToString()); +// +// // UPDATE +// DataRecord updatedRecord = new DataRecord( +// "mykey", +// DataRecord.DataRecordStatus.COMPLETED, +// now.AddSeconds(500).ToUnixTimeMilliseconds(), +// "response", +// null +// ); +// await persistenceStore.UpdateRecord(updatedRecord); +// recordInDb = await persistenceStore.GetRecord("mykey"); +// recordInDb.Should().Be(updatedRecord); +// +// // DELETE +// await persistenceStore.DeleteRecord("mykey"); +// (await client.ScanAsync(new ScanRequest +// { +// TableName = TABLE_NAME_CUSTOM +// })).Count.Should().Be(0); +// +// } +// finally +// { +// try +// { +// await client.DeleteTableAsync(new DeleteTableRequest +// { +// TableName = TABLE_NAME_CUSTOM +// }); +// } +// catch (Exception) +// { +// // OK +// } +// } +// } +// +// [Fact] +// public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() +// { +// try +// { +// // Arrange +// Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); +// +// DynamoDBPersistenceStore store = new DynamoDBPersistenceStoreBuilder().WithTableName(TABLE_NAME).Build(); +// +// // Act +// Func act = () => store.GetRecord("fake"); +// +// // Assert +// await act.Should().ThrowAsync(); +// } +// finally +// { +// Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); +// } +// } +// private static Dictionary CreateKey(string keyValue) +// { +// var key = new Dictionary() +// { +// {"id", new AttributeValue(keyValue)} +// }; +// return key; +// } +// } \ No newline at end of file From bf66bc00e0bc23dccf95eb37b845fda2c139f7af Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Fri, 5 May 2023 19:06:49 +0100 Subject: [PATCH 16/32] fix csproj --- .../AWS.Lambda.Powertools.Idempotency.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index be35e5a7..e20ed0bc 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -9,7 +9,7 @@ Amazon Web Services Amazon.com, Inc AWS Lambda Powertools for .NET - AWS Lambda Powertools for .NET - Logging package. + AWS Lambda Powertools for .NET - Idempotency package. Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. https://github.com/awslabs/aws-lambda-powertools-dotnet Apache-2.0 From 03afc4d7b99df6408e3dc1a0dad9feffae12a849 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Fri, 19 May 2023 17:22:35 +0100 Subject: [PATCH 17/32] remove null. Code updates to var. Set execution environment tests --- .../Core/Constants.cs | 5 ++ .../Core/IPowertoolsConfigurations.cs | 6 ++ .../Core/PowertoolsConfigurations.cs | 4 + .../InternalsVisibleTo.cs | 4 +- .../AWS.Lambda.Powertools.Idempotency.csproj | 3 +- .../IdempotencyAlreadyInProgressException.cs | 4 +- .../IdempotencyConfigurationException.cs | 4 +- .../IdempotencyInconsistentStateException.cs | 4 +- .../IdempotencyItemAlreadyExistsException.cs | 4 +- .../IdempotencyItemNotFoundException.cs | 4 +- .../Exceptions/IdempotencyKeyException.cs | 4 +- .../IdempotencyPersistenceLayerException.cs | 4 +- .../IdempotencyValidationException.cs | 4 +- .../Idempotency.cs | 20 +++-- .../IdempotencyOptions.cs | 8 +- .../IdempotencyOptionsBuilder.cs | 4 +- .../IdempotentAttribute.cs | 18 ++-- .../Internal/Constants.cs | 4 - ...Handler.cs => IdempotencyAspectHandler.cs} | 11 ++- .../Persistence/BasePersistenceStore.cs | 75 +++++----------- .../Persistence/DataRecord.cs | 10 +-- .../Persistence/DynamoDBPersistenceStore.cs | 48 +++++----- ...Lambda.Powertools.Idempotency.Tests.csproj | 1 + .../Handlers/IdempotencyEnabledFunction.cs | 2 +- .../Handlers/IdempotencyFunction.cs | 17 ++-- .../IdempotencyTest.cs | 12 +-- .../IntegrationTestBase.cs | 26 +++--- .../Internal/IdempotencyTests.cs | 38 ++++++++ .../Internal/IdempotentAspectTests.cs | 43 ++++----- .../Persistence/BasePersistenceStoreTests.cs | 89 ++++++++++++------- .../DynamoDBPersistenceStoreTests.cs | 5 +- 31 files changed, 272 insertions(+), 213 deletions(-) rename libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/{IdempotencyHandler.cs => IdempotencyAspectHandler.cs} (96%) create mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs index 72254726..c586d563 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs @@ -90,4 +90,9 @@ internal static class Constants /// Constant for Powertools feature identifier fo AWS_EXECUTION_ENV environment variable /// internal const string FeatureContextIdentifier = "PT"; + + /// + /// Constant for IDEMPOTENCY_DISABLED_ENV environment variable + /// + internal const string IdempotencyDisabledEnv = "POWERTOOLS_IDEMPOTENCY_DISABLED"; } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs index a2e10cc7..86edd2ba 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs @@ -126,4 +126,10 @@ public interface IPowertoolsConfigurations /// /// void SetExecutionEnvironment(T type); + + /// + /// Gets a value indicating whether [Idempotency is disabled]. + /// + /// true if [Idempotency is disabled]; otherwise, false. + bool IdempotencyDisabled { get; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs index 1d099e4d..bcab772f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs @@ -190,4 +190,8 @@ public void SetExecutionEnvironment(T type) { _systemWrapper.SetExecutionEnvironment(type); } + + /// + public bool IdempotencyDisabled => + GetEnvironmentVariableOrDefault(Constants.IdempotencyDisabledEnv, false); } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs b/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs index f88809b3..ee178f7f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/InternalsVisibleTo.cs @@ -17,7 +17,9 @@ [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Logging")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics")] +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Idempotency")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Common.Tests")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Tracing.Tests")] [assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Metrics.Tests")] -[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Logging.Tests")] \ No newline at end of file +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Logging.Tests")] +[assembly: InternalsVisibleTo("AWS.Lambda.Powertools.Idempotency.Tests")] \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index e20ed0bc..503546a2 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -5,12 +5,11 @@ default AWS.Lambda.Powertools.Idempotency 0.0.1 - enable Amazon Web Services Amazon.com, Inc AWS Lambda Powertools for .NET AWS Lambda Powertools for .NET - Idempotency package. - Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. https://github.com/awslabs/aws-lambda-powertools-dotnet Apache-2.0 AWS;Amazon;Lambda;Powertools diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs index 5b5f931f..3732c573 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyAlreadyInProgressException.cs @@ -33,12 +33,12 @@ public IdempotencyAlreadyInProgressException() } /// - public IdempotencyAlreadyInProgressException(string? message) : base(message) + public IdempotencyAlreadyInProgressException(string message) : base(message) { } /// - public IdempotencyAlreadyInProgressException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyAlreadyInProgressException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs index 3c7e65d0..cec6f729 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyConfigurationException.cs @@ -33,12 +33,12 @@ public IdempotencyConfigurationException() } /// - public IdempotencyConfigurationException(string? message) : base(message) + public IdempotencyConfigurationException(string message) : base(message) { } /// - public IdempotencyConfigurationException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyConfigurationException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs index 5c5cab66..8478a813 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyInconsistentStateException.cs @@ -31,12 +31,12 @@ public IdempotencyInconsistentStateException() } /// - public IdempotencyInconsistentStateException(string? message) : base(message) + public IdempotencyInconsistentStateException(string message) : base(message) { } /// - public IdempotencyInconsistentStateException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyInconsistentStateException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs index b3f95ab1..1603bba7 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemAlreadyExistsException.cs @@ -30,12 +30,12 @@ public IdempotencyItemAlreadyExistsException() } /// - public IdempotencyItemAlreadyExistsException(string? message) : base(message) + public IdempotencyItemAlreadyExistsException(string message) : base(message) { } /// - public IdempotencyItemAlreadyExistsException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyItemAlreadyExistsException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs index bcba969c..8be4180d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyItemNotFoundException.cs @@ -30,12 +30,12 @@ public IdempotencyItemNotFoundException() } /// - public IdempotencyItemNotFoundException(string? message) : base(message) + public IdempotencyItemNotFoundException(string message) : base(message) { } /// - public IdempotencyItemNotFoundException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyItemNotFoundException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs index ba744111..46529872 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyKeyException.cs @@ -31,12 +31,12 @@ public IdempotencyKeyException() } /// - public IdempotencyKeyException(string? message) : base(message) + public IdempotencyKeyException(string message) : base(message) { } /// - public IdempotencyKeyException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyKeyException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs index a0918c39..51377386 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyPersistenceLayerException.cs @@ -30,12 +30,12 @@ public IdempotencyPersistenceLayerException() } /// - public IdempotencyPersistenceLayerException(string? message) : base(message) + public IdempotencyPersistenceLayerException(string message) : base(message) { } /// - public IdempotencyPersistenceLayerException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyPersistenceLayerException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs index ff51112e..eb03f8b5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Exceptions/IdempotencyValidationException.cs @@ -31,12 +31,12 @@ public IdempotencyValidationException() } /// - public IdempotencyValidationException(string? message) : base(message) + public IdempotencyValidationException(string message) : base(message) { } /// - public IdempotencyValidationException(string? message, Exception? innerException) : base(message, innerException) + public IdempotencyValidationException(string message, Exception innerException) : base(message, innerException) { } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 02a0572b..1df97fb1 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -14,6 +14,7 @@ */ using System; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Persistence; namespace AWS.Lambda.Powertools.Idempotency; @@ -37,8 +38,9 @@ public sealed class Idempotency /// public BasePersistenceStore PersistenceStore { get; private set; } = null!; - private Idempotency() + internal Idempotency(IPowertoolsConfigurations powertoolsConfigurations) { + powertoolsConfigurations.SetExecutionEnvironment(this); } private void SetConfig(IdempotencyOptions options) @@ -59,7 +61,7 @@ static Holder() { } - internal static readonly Idempotency IdempotencyInstance = new Idempotency(); + internal static readonly Idempotency IdempotencyInstance = new Idempotency(PowertoolsConfigurations.Instance); } /// @@ -95,11 +97,11 @@ public static void Configure(Action configurationAction) /// public class IdempotencyBuilder { - private IdempotencyOptions? _options; - private BasePersistenceStore? _store; + private IdempotencyOptions _options; + private BasePersistenceStore _store; - internal IdempotencyOptions? Options => _options; - internal BasePersistenceStore? Store => _store; + internal IdempotencyOptions Options => _options; + internal BasePersistenceStore Store => _store; /// /// Set the persistence layer to use for storing the request and response @@ -119,7 +121,7 @@ public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceS /// public IdempotencyBuilder UseDynamoDb(Action builderAction) { - DynamoDBPersistenceStoreBuilder builder = + var builder = new DynamoDBPersistenceStoreBuilder(); builderAction(builder); _store = builder.Build(); @@ -133,7 +135,7 @@ public IdempotencyBuilder UseDynamoDb(Action bu /// public IdempotencyBuilder UseDynamoDb(string tableName) { - DynamoDBPersistenceStoreBuilder builder = + var builder = new DynamoDBPersistenceStoreBuilder(); _store = builder.WithTableName(tableName).Build(); return this; @@ -146,7 +148,7 @@ public IdempotencyBuilder UseDynamoDb(string tableName) /// public IdempotencyBuilder WithOptions(Action builderAction) { - IdempotencyOptionsBuilder builder = new IdempotencyOptionsBuilder(); + var builder = new IdempotencyOptionsBuilder(); builderAction(builder); _options = builder.Build(); return this; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs index cfbc60b0..f74b094f 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs @@ -31,12 +31,12 @@ public class IdempotencyOptions /// Records[0].Sns.Message | powertools_json(@) for SNSEvent /// Detail for ScheduledEvent (EventBridge / CloudWatch events) /// - public string? EventKeyJmesPath { get; } + public string EventKeyJmesPath { get; } /// /// JMES Path of a part of the payload to be used for validation /// See https://jmespath.org/ /// - public string? PayloadValidationJmesPath { get; } + public string PayloadValidationJmesPath { get; } /// /// Boolean to indicate if we must throw an Exception when /// idempotency key could not be found in the payload. @@ -66,8 +66,8 @@ public class IdempotencyOptions public ILog Log { get; } internal IdempotencyOptions( - string? eventKeyJmesPath, - string? payloadValidationJmesPath, + string eventKeyJmesPath, + string payloadValidationJmesPath, bool throwOnNoIdempotencyKey, bool useLocalCache, int localCacheMaxItems, diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs index 95de4904..181044bc 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs @@ -11,8 +11,8 @@ public class IdempotencyOptionsBuilder private int _localCacheMaxItems = 256; private bool _useLocalCache = false; private long _expirationInSeconds = 60 * 60; // 1 hour - private string? _eventKeyJmesPath = null; - private string? _payloadValidationJmesPath = null; + private string _eventKeyJmesPath = null; + private string _payloadValidationJmesPath = null; private bool _throwOnNoIdempotencyKey = false; private string _hashFunction = "MD5"; private ILog _log = new NullLog(); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 9bb2c19d..e308e2a5 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -20,6 +20,7 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; +using Constants = AWS.Lambda.Powertools.Common.Constants; namespace AWS.Lambda.Powertools.Idempotency; @@ -67,11 +68,11 @@ public class IdempotentAttribute : UniversalWrapperAttribute /// The arguments. /// The instance containing the event data. /// T. - protected sealed override T WrapSync(Func target, object[] args, AspectEventArgs eventArgs) + protected internal sealed override T WrapSync(Func target, object[] args, AspectEventArgs eventArgs) { throw new IdempotencyConfigurationException("Idempotent attribute can be used on async methods only"); } - + /// /// Wrap as an asynchronous operation. /// @@ -80,27 +81,24 @@ protected sealed override T WrapSync(Func target, object[] args, /// The arguments. /// The instance containing the event data. /// A Task<T> representing the asynchronous operation. - protected sealed override async Task WrapAsync( + protected internal sealed override async Task WrapAsync( Func> target, object[] args, AspectEventArgs eventArgs) { - - string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); + var idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); if (idempotencyDisabledEnv is "true") { return await base.WrapAsync(target, args, eventArgs); } - var payload = JsonSerializer.SerializeToDocument(args[0]); + var payload = JsonDocument.Parse(JsonSerializer.Serialize(args[0])); if (payload == null) { throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); } - var types = new[] {typeof(T)}; - var genericType = typeof(IdempotencyHandler<>).MakeGenericType(types); - var idempotencyHandler = Activator.CreateInstance(genericType,target, args, eventArgs.Method.Name, payload) as IdempotencyHandler; + var idempotencyHandler = new IdempotencyAspectHandler(target, args, eventArgs.Method.Name, payload); if (idempotencyHandler == null) { - throw new Exception("Failed to create an instance of IdempotencyHandler"); + throw new Exception("Failed to create an instance of IdempotencyAspectHandler"); } var result = await idempotencyHandler.Handle(); return result; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs index 90f8578a..7e757032 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs @@ -27,8 +27,4 @@ internal class Constants { /// Constant for AWS_REGION_ENV environment variable /// internal const string AwsRegionEnv = "AWS_REGION"; - /// - /// Constant for IDEMPOTENCY_DISABLED_ENV environment variable - /// - internal const string IdempotencyDisabledEnv = "POWERTOOLS_IDEMPOTENCY_DISABLED"; } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs similarity index 96% rename from libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs rename to libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs index 6599b574..eaa12860 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs @@ -15,7 +15,6 @@ using System; using System.Text.Json; -using System.Text.Json.Nodes; using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Output; @@ -24,7 +23,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Internal; -internal class IdempotencyHandler +internal class IdempotencyAspectHandler { private const int MaxRetries = 2; @@ -34,7 +33,7 @@ internal class IdempotencyHandler private readonly BasePersistenceStore _persistenceStore; private readonly ILog _log; - public IdempotencyHandler( + public IdempotencyAspectHandler( Func target, object[] args, string functionName, @@ -57,7 +56,7 @@ public async Task Handle() // IdempotencyInconsistentStateException can happen under rare but expected cases // when persistent state changes in the small time between put & get requests. // In most cases we can retry successfully on this exception. - for (int i = 0; true; i++) + for (var i = 0; true; i++) { try { @@ -89,7 +88,7 @@ private async Task ProcessIdempotency() } catch (IdempotencyItemAlreadyExistsException) { - DataRecord record = await GetIdempotencyRecord(); + var record = await GetIdempotencyRecord(); return await HandleForStatus(record); } catch (IdempotencyKeyException) @@ -167,7 +166,7 @@ private Task HandleForStatus(DataRecord record) try { _log.WriteDebug("Response for key '{0}' retrieved from idempotency store, skipping the function", record.IdempotencyKey); - T? result = JsonSerializer.Deserialize(record.ResponseData!); + var result = JsonSerializer.Deserialize(record.ResponseData!); if (result is null) { throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index d17443a6..e21dce81 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -17,7 +17,6 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using System.Text.Json.Nodes; using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; @@ -35,7 +34,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; public abstract class BasePersistenceStore : IPersistenceStore { private IdempotencyOptions _idempotencyOptions = null!; - private string? _functionName; + private string _functionName; /// /// Boolean to indicate whether or not payload validation is enabled /// @@ -51,10 +50,10 @@ public abstract class BasePersistenceStore : IPersistenceStore /// /// Idempotency configuration settings /// The name of the function being decorated - public void Configure(IdempotencyOptions idempotencyOptions, string? functionName) + public void Configure(IdempotencyOptions idempotencyOptions, string functionName) { - string? funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv); - _functionName = funcEnv != null ? funcEnv : "testFunction"; + var funcEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv); + _functionName = funcEnv ?? "testFunction"; if (!string.IsNullOrWhiteSpace(functionName)) { _functionName += "." + functionName; @@ -71,7 +70,7 @@ public void Configure(IdempotencyOptions idempotencyOptions, string? functionNam var useLocalCache = _idempotencyOptions.UseLocalCache; if (useLocalCache) { - _cache = new (_idempotencyOptions.LocalCacheMaxItems); + _cache = new LRUCache(_idempotencyOptions.LocalCacheMaxItems); } } @@ -92,7 +91,7 @@ internal void Configure(IdempotencyOptions options, string functionName, LRUCach /// The current date time public virtual async Task SaveSuccess(JsonDocument data, object result, DateTimeOffset now) { - string responseJson = JsonSerializer.Serialize(result); + var responseJson = JsonSerializer.Serialize(result); var record = new DataRecord( GetHashedIdempotencyKey(data), DataRecord.DataRecordStatus.COMPLETED, @@ -113,14 +112,14 @@ public virtual async Task SaveSuccess(JsonDocument data, object result, DateTime /// public virtual async Task SaveInProgress(JsonDocument data, DateTimeOffset now) { - string idempotencyKey = GetHashedIdempotencyKey(data); + var idempotencyKey = GetHashedIdempotencyKey(data); if (RetrieveFromCache(idempotencyKey, now) != null) { throw new IdempotencyItemAlreadyExistsException(); } - DataRecord record = new DataRecord( + var record = new DataRecord( idempotencyKey, DataRecord.DataRecordStatus.INPROGRESS, GetExpiryEpochSecond(now), @@ -138,7 +137,7 @@ public virtual async Task SaveInProgress(JsonDocument data, DateTimeOffset now) /// The throwable thrown by the function public virtual async Task DeleteRecord(JsonDocument data, Exception throwable) { - string idemPotencyKey = GetHashedIdempotencyKey(data); + var idemPotencyKey = GetHashedIdempotencyKey(data); Log.WriteDebug("Function raised an exception {0}. " + "Clearing in progress record in persistence store for idempotency key: {1}", @@ -157,9 +156,9 @@ public virtual async Task DeleteRecord(JsonDocument data, Exception throwable) /// DataRecord representation of existing record found in persistence store public virtual async Task GetRecord(JsonDocument data, DateTimeOffset now) { - string idempotencyKey = GetHashedIdempotencyKey(data); + var idempotencyKey = GetHashedIdempotencyKey(data); - DataRecord? cachedRecord = RetrieveFromCache(idempotencyKey, now); + var cachedRecord = RetrieveFromCache(idempotencyKey, now); if (cachedRecord != null) { Log.WriteDebug("Idempotency record found in cache with idempotency key: {0}", idempotencyKey); @@ -167,7 +166,7 @@ public virtual async Task GetRecord(JsonDocument data, DateTimeOffse return cachedRecord; } - DataRecord record = await GetRecord(idempotencyKey); + var record = await GetRecord(idempotencyKey); SaveToCache(record); ValidatePayload(data, record); return record; @@ -197,23 +196,21 @@ private void SaveToCache(DataRecord dataRecord) /// private void ValidatePayload(JsonDocument data, DataRecord dataRecord) { - if (PayloadValidationEnabled) - { - string dataHash = GetHashedPayload(data); + if (!PayloadValidationEnabled) return; + var dataHash = GetHashedPayload(data); - if (dataHash != dataRecord.PayloadHash) - { - throw new IdempotencyValidationException("Payload does not match stored record for this event key"); - } + if (dataHash != dataRecord.PayloadHash) + { + throw new IdempotencyValidationException("Payload does not match stored record for this event key"); } } - private DataRecord? RetrieveFromCache(string idempotencyKey, DateTimeOffset now) + private DataRecord RetrieveFromCache(string idempotencyKey, DateTimeOffset now) { if (!_idempotencyOptions.UseLocalCache) return null; - if (_cache.TryGet(idempotencyKey, out DataRecord record) && record!=null) + if (_cache.TryGet(idempotencyKey, out var record) && record!=null) { if (!record.IsExpired(now)) { @@ -290,7 +287,7 @@ private string GetHashedIdempotencyKey(JsonDocument data) Log.WriteWarning("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyOptions.EventKeyJmesPath ?? string.Empty); } - string hash = GenerateHash(node); + var hash = GenerateHash(node); return _functionName + "#" + hash; } @@ -312,37 +309,13 @@ private bool IsMissingIdemPotencyKey(JsonElement data) /// internal string GenerateHash(JsonElement data) { - var node = data; - // if (data is JObject or JArray) // if array or object, use the json string representation, otherwise get the real value - // { - // node = data; - // } - // else if (data.Type == JsonNodeType.String) - // { - // node = data.Value(); - // } - // else if (data.Type == JsonNodeType.Integer) - // { - // node = data.Value(); - // } - // else if (data.Type == JsonNodeType.Float) - // { - // node = data.Value(); - // } - // else if (data.Type == JsonNodeType.Boolean) - // { - // node = data.Value(); - // } - // else node = data; // anything else - - using var hashAlgorithm = HashAlgorithm.Create(_idempotencyOptions.HashFunction); if (hashAlgorithm == null) { throw new ArgumentException("Invalid HashAlgorithm"); } - var stringToHash = node.ToString(); - string hash = GetHash(hashAlgorithm, stringToHash); + var stringToHash = data.ToString(); + var hash = GetHash(hashAlgorithm, stringToHash); return hash; } @@ -350,7 +323,7 @@ internal string GenerateHash(JsonElement data) private static string GetHash(HashAlgorithm hashAlgorithm, string input) { // Convert the input string to a byte array and compute the hash. - byte[] data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input)); + var data = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input)); // Create a new Stringbuilder to collect the bytes // and create a string. @@ -358,7 +331,7 @@ private static string GetHash(HashAlgorithm hashAlgorithm, string input) // Loop through each byte of the hashed data // and format each one as a hexadecimal string. - for (int i = 0; i < data.Length; i++) + for (var i = 0; i < data.Length; i++) { sBuilder.Append(data[i].ToString("x2")); } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs index 043942d4..2144bc50 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs @@ -35,8 +35,8 @@ public class DataRecord public DataRecord(string idempotencyKey, DataRecordStatus status, long expiryTimestamp, - string? responseData, - string? payloadHash) + string responseData, + string payloadHash) { IdempotencyKey = idempotencyKey; _status = status.ToString(); @@ -56,11 +56,11 @@ public DataRecord(string idempotencyKey, /// /// JSON serialized invocation results /// - public string? ResponseData { get; } + public string ResponseData { get; } /// /// A hash representation of the entire event /// - public string? PayloadHash { get; } + public string PayloadHash { get; } /// @@ -106,7 +106,7 @@ protected bool Equals(DataRecord other) } /// - public override bool Equals(object? obj) + public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index b96e7557..88204262 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -19,8 +19,9 @@ using Amazon; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; -using AWS.Lambda.Powertools.Idempotency.Internal; +using Constants = AWS.Lambda.Powertools.Idempotency.Internal.Constants; namespace AWS.Lambda.Powertools.Idempotency.Persistence; @@ -33,22 +34,22 @@ public class DynamoDBPersistenceStore : BasePersistenceStore private readonly string _tableName; private readonly string _keyAttr; private readonly string _staticPkValue; - private readonly string? _sortKeyAttr; + private readonly string _sortKeyAttr; private readonly string _expiryAttr; private readonly string _statusAttr; private readonly string _dataAttr; private readonly string _validationAttr; - private readonly AmazonDynamoDBClient? _dynamoDbClient; + private readonly AmazonDynamoDBClient _dynamoDbClient; internal DynamoDBPersistenceStore(string tableName, string keyAttr, string staticPkValue, - string? sortKeyAttr, + string sortKeyAttr, string expiryAttr, string statusAttr, string dataAttr, string validationAttr, - AmazonDynamoDBClient? client) + AmazonDynamoDBClient client) { _tableName = tableName; _keyAttr = keyAttr; @@ -65,10 +66,9 @@ internal DynamoDBPersistenceStore(string tableName, } else { - string? idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); - if (idempotencyDisabledEnv == null || idempotencyDisabledEnv.Equals("false")) + if (PowertoolsConfigurations.Instance.IdempotencyDisabled) { - AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig + var clientConfig = new AmazonDynamoDBConfig { RegionEndpoint = RegionEndpoint.GetBySystemName(Environment.GetEnvironmentVariable(Constants.AwsRegionEnv)) }; @@ -91,7 +91,7 @@ public override async Task GetRecord(string idempotencyKey) ConsistentRead = true, Key = GetKey(idempotencyKey) }; - GetItemResponse response = await _dynamoDbClient!.GetItemAsync(getItemRequest); + var response = await _dynamoDbClient!.GetItemAsync(getItemRequest); if (!response.IsItemSet) { @@ -105,12 +105,16 @@ public override async Task GetRecord(string idempotencyKey) /// public override async Task PutRecord(DataRecord record, DateTimeOffset now) { - Dictionary item = new(GetKey(record.IdempotencyKey)); - item.Add(this._expiryAttr, new AttributeValue() + Dictionary item = new(GetKey(record.IdempotencyKey)) { - N = record.ExpiryTimestamp.ToString() - }); - item.Add(this._statusAttr, new AttributeValue(record.Status.ToString())); + { + this._expiryAttr, new AttributeValue() + { + N = record.ExpiryTimestamp.ToString() + } + }, + { this._statusAttr, new AttributeValue(record.Status.ToString()) } + }; if (PayloadValidationEnabled) { @@ -127,7 +131,7 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) {"#expiry", this._expiryAttr} }; - PutItemRequest request = new PutItemRequest() + var request = new PutItemRequest() { TableName = _tableName, Item = item, @@ -153,7 +157,7 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) public override async Task UpdateRecord(DataRecord record) { Log.WriteDebug("Updating record for idempotency key: {0}", record.IdempotencyKey); - string updateExpression = "SET #response_data = :response_data, #expiry = :expiry, #status = :status"; + var updateExpression = "SET #response_data = :response_data, #expiry = :expiry, #status = :status"; var expressionAttributeNames = new Dictionary { @@ -207,10 +211,10 @@ public override async Task DeleteRecord(string idempotencyKey) private DataRecord ItemToRecord(Dictionary item) { // data and validation payload may be null - var hasDataAttribute = item.TryGetValue(_dataAttr, out AttributeValue? data); - var hasValidationAttribute = item.TryGetValue(_validationAttr, out AttributeValue? validation); + var hasDataAttribute = item.TryGetValue(_dataAttr, out var data); + var hasValidationAttribute = item.TryGetValue(_validationAttr, out var validation); - return new DataRecord(item[_sortKeyAttr != null ? _sortKeyAttr : _keyAttr].S, + return new DataRecord(item[_sortKeyAttr ?? _keyAttr].S, Enum.Parse(item[_statusAttr].S), long.Parse(item[_expiryAttr].N), hasDataAttribute ? data?.S : null, @@ -249,17 +253,17 @@ private Dictionary GetKey(string idempotencyKey) // ReSharper disable once InconsistentNaming public class DynamoDBPersistenceStoreBuilder { - private static readonly string? FuncEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv); + private static readonly string FuncEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv); private string _tableName = null!; private string _keyAttr = "id"; private string _staticPkValue = string.Format("idempotency#%s", FuncEnv ?? ""); - private string? _sortKeyAttr = null; + private string _sortKeyAttr; private string _expiryAttr = "expiration"; private string _statusAttr = "status"; private string _dataAttr = "data"; private string _validationAttr = "validation"; - private AmazonDynamoDBClient? _dynamoDbClient; + private AmazonDynamoDBClient _dynamoDbClient; /// /// Initialize and return a new instance of {@link DynamoDBPersistenceStore}. diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj index 8d68afb8..12a4cc58 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj @@ -27,6 +27,7 @@ + diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs index 272a88c6..cd88eba8 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs @@ -27,7 +27,7 @@ public class IdempotencyEnabledFunction public Task Handle(Product input, ILambdaContext context) { HandlerExecuted = true; - Basket basket = new Basket(); + var basket = new Basket(); basket.Add(input); var result = Task.FromResult(basket); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs index 76242121..8925b089 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs @@ -19,6 +19,7 @@ using System.Net.Http; using System.Text.Json; using System.Text.Json.Nodes; +using System.Threading; using System.Threading.Tasks; using Amazon; using Amazon.DynamoDBv2; @@ -37,6 +38,7 @@ public IdempotencyFunction(AmazonDynamoDBClient client) builder .WithOptions(optionsBuilder => optionsBuilder + //.WithUseLocalCache(true) .WithEventKeyJmesPath("powertools_json(Body).address") .WithExpiration(TimeSpan.FromSeconds(20))) .UseDynamoDb(storeBuilder => @@ -50,6 +52,7 @@ public IdempotencyFunction(AmazonDynamoDBClient client) public async Task Handle(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) { HandlerExecuted = true; + var result= await InternalFunctionHandler(apigProxyEvent,context); return result; @@ -66,9 +69,9 @@ private async Task InternalFunctionHandler(APIGatewayPr try { - string address = JsonDocument.Parse(apigProxyEvent.Body).RootElement.GetProperty("address").GetString(); - string pageContents = await GetPageContents(address); - string output = $"{{ \"message\": \"hello world\", \"location\": \"{pageContents}\" }}"; + var address = JsonDocument.Parse(apigProxyEvent.Body).RootElement.GetProperty("address").GetString(); + var pageContents = await GetPageContents(address); + var output = $"{{ \"message\": \"hello world\", \"location\": \"{pageContents}\" }}"; return new APIGatewayProxyResponse { @@ -92,10 +95,10 @@ private async Task InternalFunctionHandler(APIGatewayPr // we could actually also put the @Idempotent annotation here private async Task GetPageContents(string address) { - HttpClient client = new HttpClient(); - using HttpResponseMessage response = await client.GetAsync(address); - using HttpContent content = response.Content; - string pageContent = await content.ReadAsStringAsync(); + var client = new HttpClient(); + using var response = await client.GetAsync(address); + using var content = response.Content; + var pageContent = await content.ReadAsStringAsync(); return pageContent; } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs index 6e1ea231..802bd21b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -29,14 +29,14 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests; public class IdempotencyTest { - protected const string TABLE_NAME = "idempotency_table"; + private const string TableName = "idempotency_table"; [Fact] public async Task EndToEndTest() { var client = new AmazonDynamoDBClient(); - IdempotencyFunction function = new IdempotencyFunction(client); + var function = new IdempotencyFunction(client); var options = new JsonSerializerOptions { @@ -44,10 +44,10 @@ public async Task EndToEndTest() }; //var persistenceStore = new InMemoryPersistenceStore(); - TestLambdaContext context = new TestLambdaContext(); - var request = JsonSerializer.Deserialize(File.ReadAllText("./resources/apigw_event2.json"),options); + var context = new TestLambdaContext(); + var request = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./resources/apigw_event2.json"),options); - APIGatewayProxyResponse response = await function.Handle(request, context); + var response = await function.Handle(request, context); function.HandlerExecuted.Should().BeTrue(); function.HandlerExecuted = false; @@ -60,7 +60,7 @@ public async Task EndToEndTest() var scanResponse = await client.ScanAsync(new ScanRequest { - TableName = TABLE_NAME + TableName = TableName }); scanResponse.Count.Should().Be(1); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs index 3661977e..31e074bd 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs @@ -26,23 +26,25 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests; public class IntegrationTestBase : IAsyncLifetime { - protected const string TABLE_NAME = "idempotency_table"; - protected AmazonDynamoDBClient client; - private protected DynamoDBPersistenceStore _dynamoDbPersistenceStore; + protected const string TableName = "idempotency_table"; + protected AmazonDynamoDBClient Client; + private protected DynamoDBPersistenceStore DynamoDbPersistenceStore; public async Task InitializeAsync() { + // initialize TestContainers or Ductus.FluentDocker or have DynamoDb local docker running + var credentials = new BasicAWSCredentials("FAKE", "FAKE"); var amazonDynamoDbConfig = new AmazonDynamoDBConfig() { ServiceURL = new UriBuilder("http", "localhost", 8000).Uri.ToString(), AuthenticationRegion = "us-east-1" }; - client = new AmazonDynamoDBClient(credentials, amazonDynamoDbConfig); + Client = new AmazonDynamoDBClient(credentials, amazonDynamoDbConfig); var createTableRequest = new CreateTableRequest { - TableName = TABLE_NAME, + TableName = TableName, KeySchema = new List() { new("id", KeyType.HASH) @@ -59,8 +61,8 @@ public async Task InitializeAsync() }; try { - await client.CreateTableAsync(createTableRequest); - var response = await client.DescribeTableAsync(TABLE_NAME); + await Client.CreateTableAsync(createTableRequest); + var response = await Client.DescribeTableAsync(TableName); if (response == null) { throw new NullReferenceException("Table was not created within the expected time"); @@ -71,17 +73,17 @@ public async Task InitializeAsync() Console.WriteLine(e.Message); } - _dynamoDbPersistenceStore = new DynamoDBPersistenceStoreBuilder() - .WithTableName(TABLE_NAME) - .WithDynamoDBClient(client) + DynamoDbPersistenceStore = new DynamoDBPersistenceStoreBuilder() + .WithTableName(TableName) + .WithDynamoDBClient(Client) .Build(); - _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); + DynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); } public Task DisposeAsync() { // Make sure delete item after each test - _dynamoDbPersistenceStore.DeleteRecord("key").ConfigureAwait(false); + DynamoDbPersistenceStore.DeleteRecord("key").ConfigureAwait(false); //_dynamoContainer.DisposeAsync().ConfigureAwait(false); return Task.CompletedTask; } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs new file mode 100644 index 00000000..daeb2166 --- /dev/null +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs @@ -0,0 +1,38 @@ +using AWS.Lambda.Powertools.Common; +using Moq; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; + +public class IdempotencyTests +{ + [Fact] + public void Idempotency_Set_Execution_Environment_Context() + { + // Arrange + var assemblyName = "AWS.Lambda.Powertools.Idempotency"; + var assemblyVersion = "1.0.0"; + + var env = new Mock(); + env.Setup(x => x.GetAssemblyName(It.IsAny())).Returns(assemblyName); + env.Setup(x => x.GetAssemblyVersion(It.IsAny())).Returns(assemblyVersion); + + var conf = new PowertoolsConfigurations(new SystemWrapper(env.Object)); + + // Act + var xRayRecorder = new Idempotency(conf); + + // Assert + env.Verify(v => + v.SetEnvironmentVariable( + "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Idempotency/{assemblyVersion}" + ), Times.Once); + + env.Verify(v => + v.GetEnvironmentVariable( + "AWS_EXECUTION_ENV" + ), Times.Once); + + Assert.NotNull(xRayRecorder); + } +} \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index e7293fc4..f7a9033e 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -15,11 +15,10 @@ using System; using System.Text.Json; -using System.Text.Json.Nodes; using System.Threading.Tasks; using Amazon.Lambda.TestUtilities; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; -using AWS.Lambda.Powertools.Idempotency.Internal; using AWS.Lambda.Powertools.Idempotency.Persistence; using AWS.Lambda.Powertools.Idempotency.Tests.Handlers; using AWS.Lambda.Powertools.Idempotency.Tests.Model; @@ -27,8 +26,11 @@ using Moq; using Xunit; +[assembly: CollectionBehavior(DisableTestParallelization = true)] + namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; +[Collection("Sequential")] public class IdempotentAspectTests { [Fact] @@ -42,11 +44,11 @@ public async Task Handle_WhenFirstCall_ShouldPutInStore() .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); - Product product = new Product(42, "fake product", 12); + var function = new IdempotencyEnabledFunction(); + var product = new Product(42, "fake product", 12); //Act - Basket basket = await function.Handle(product, new TestLambdaContext()); + var basket = await function.Handle(product, new TestLambdaContext()); //Assert basket.Products.Count.Should().Be(1); @@ -74,9 +76,9 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - Product product = new Product(42, "fake product", 12); - Basket basket = new Basket(product); - DataRecord record = new DataRecord( + var product = new Product(42, "fake product", 12); + var basket = new Basket(product); + var record = new DataRecord( "42", DataRecord.DataRecordStatus.COMPLETED, DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), @@ -85,10 +87,10 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); - IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + var function = new IdempotencyEnabledFunction(); // Act - Basket resultBasket = await function.Handle(product, new TestLambdaContext()); + var resultBasket = await function.Handle(product, new TestLambdaContext()); // Assert resultBasket.Should().Be(basket); @@ -109,9 +111,9 @@ public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempoten store.Setup(x=>x.SaveInProgress(It.IsAny(), It.IsAny())) .Throws(); - Product product = new Product(42, "fake product", 12); - Basket basket = new Basket(product); - DataRecord record = new DataRecord( + var product = new Product(42, "fake product", 12); + var basket = new Basket(product); + var record = new DataRecord( "42", DataRecord.DataRecordStatus.INPROGRESS, DateTimeOffset.UtcNow.AddSeconds(356).ToUnixTimeSeconds(), @@ -121,13 +123,13 @@ public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempoten .ReturnsAsync(record); // Act - IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); + var function = new IdempotencyEnabledFunction(); Func act = async () => await function.Handle(product, new TestLambdaContext()); // Assert await act.Should().ThrowAsync(); } - + [Fact] public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionException() { @@ -140,8 +142,8 @@ public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionE .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - IdempotencyWithErrorFunction function = new IdempotencyWithErrorFunction(); - Product product = new Product(42, "fake product", 12); + var function = new IdempotencyWithErrorFunction(); + var product = new Product(42, "fake product", 12); // Act Func act = async () => await function.Handle(product, new TestLambdaContext()); @@ -159,6 +161,7 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() { // Arrange var store = new Mock(); + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); Idempotency.Configure(builder => @@ -167,11 +170,11 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - IdempotencyEnabledFunction function = new IdempotencyEnabledFunction(); - Product product = new Product(42, "fake product", 12); + var function = new IdempotencyEnabledFunction(); + var product = new Product(42, "fake product", 12); // Act - Basket basket = await function.Handle(product, new TestLambdaContext()); + var basket = await function.Handle(product, new TestLambdaContext()); // Assert store.Invocations.Count.Should().Be(0); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 9dd912dc..5b7f4d79 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -78,7 +78,7 @@ public async Task SaveInProgress_WhenDefaultConfig_ShouldSaveRecordInStore() persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; // Act await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); @@ -104,7 +104,7 @@ public async Task SaveInProgress_WhenKeyJmesPathIsSet_ShouldSaveRecordInStore_Wi .WithEventKeyJmesPath("powertools_json(Body).id") .Build(), "myfunc"); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; // Act await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); @@ -119,6 +119,32 @@ public async Task SaveInProgress_WhenKeyJmesPathIsSet_ShouldSaveRecordInStore_Wi persistenceStore.Status.Should().Be(1); } + [Fact] + public async Task SaveInProgress_WhenKeyJmesPathIsSetToMultipleFields_ShouldSaveRecordInStore_WithIdempotencyKeyEqualsKeyJmesPath() + { + // Arrange + var persistenceStore = new InMemoryPersistenceStore(); + var request = LoadApiGatewayProxyRequest(); + + persistenceStore.Configure(new IdempotencyOptionsBuilder() + .WithEventKeyJmesPath("powertools_json(Body).[id, message]") //[43876123454654,"Lambda rocks"] + .Build(), "myfunc"); + + var now = DateTimeOffset.UtcNow; + + // Act + await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); + + // Assert + var dr = persistenceStore.DataRecord; + dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); + dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); + dr.ResponseData.Should().BeNull(); + dr.IdempotencyKey.Should().Be("testFunction.myfunc#5ca4c8c44d427e9d43ca918a24d6cf42"); + dr.PayloadHash.Should().BeEmpty(); + persistenceStore.Status.Should().Be(1); + } + [Fact] public async Task SaveInProgress_WhenJMESPath_NotFound_ShouldThrowException() @@ -131,10 +157,10 @@ public async Task SaveInProgress_WhenJMESPath_NotFound_ShouldThrowException() .WithEventKeyJmesPath("unavailable") .WithThrowOnNoIdempotencyKey(true) // should throw .Build(), ""); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; // Act - Func act = async () => await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); + var act = async () => await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert await act.Should() @@ -155,13 +181,13 @@ public async Task SaveInProgress_WhenJMESpath_NotFound_ShouldNotThrowException() .WithEventKeyJmesPath("unavailable") .Build(), ""); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; // Act await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert - DataRecord dr = persistenceStore.DataRecord; + var dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); persistenceStore.Status.Should().Be(1); } @@ -179,7 +205,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSet_AndNotExpired_ShouldThrowEx .WithEventKeyJmesPath("powertools_json(Body).id") .Build(), null, cache); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; cache.Set("testFunction#2fef178cc82be5ce3da6c5e0466a6182", new DataRecord( "testFunction#2fef178cc82be5ce3da6c5e0466a6182", @@ -189,7 +215,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSet_AndNotExpired_ShouldThrowEx ); // Act - Func act = () => persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); + var act = () => persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert await act.Should() @@ -212,7 +238,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSetButExpired_ShouldRemoveFromC .WithExpiration(TimeSpan.FromSeconds(2)) .Build(), null, cache); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; cache.Set("testFunction#2fef178cc82be5ce3da6c5e0466a6182", new DataRecord( "testFunction#2fef178cc82be5ce3da6c5e0466a6182", @@ -225,7 +251,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSetButExpired_ShouldRemoveFromC await persistenceStore.SaveInProgress(JsonSerializer.SerializeToDocument(request)!, now); // Assert - DataRecord dr = persistenceStore.DataRecord; + var dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); cache.Count.Should().Be(0); persistenceStore.Status.Should().Be(1); @@ -242,15 +268,15 @@ public async Task SaveSuccess_WhenDefaultConfig_ShouldUpdateRecord() LRUCache cache = new ((int) 2); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null, cache); - Product product = new Product(34543, "product", 42); + var product = new Product(34543, "product", 42); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; // Act await persistenceStore.SaveSuccess(JsonSerializer.SerializeToDocument(request)!, product, now); // Assert - DataRecord dr = persistenceStore.DataRecord; + var dr = persistenceStore.DataRecord; dr.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); dr.ResponseData.Should().Be(JsonSerializer.Serialize(product)); @@ -271,8 +297,8 @@ public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), null, cache); - Product product = new Product(34543, "product", 42); - DateTimeOffset now = DateTimeOffset.UtcNow; + var product = new Product(34543, "product", 42); + var now = DateTimeOffset.UtcNow; // Act await persistenceStore.SaveSuccess(JsonSerializer.SerializeToDocument(request)!, product, now); @@ -281,7 +307,7 @@ public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() persistenceStore.Status.Should().Be(2); cache.Count.Should().Be(1); - var foundDataRecord = cache.TryGet("testFunction#b105f675a45bab746c0723da594d3b06", out DataRecord record); + var foundDataRecord = cache.TryGet("testFunction#b105f675a45bab746c0723da594d3b06", out var record); foundDataRecord.Should().BeTrue(); record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); record.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); @@ -302,10 +328,10 @@ public async Task GetRecord_WhenRecordIsInStore_ShouldReturnRecordFromPersistenc LRUCache cache = new((int) 2); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), "myfunc", cache); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; // Act - DataRecord record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); + var record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); @@ -325,8 +351,8 @@ public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCac persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); - DateTimeOffset now = DateTimeOffset.UtcNow; - DataRecord dr = new DataRecord( + var now = DateTimeOffset.UtcNow; + var dr = new DataRecord( "testFunction.myfunc#b105f675a45bab746c0723da594d3b06", DataRecord.DataRecordStatus.COMPLETED, now.AddSeconds(3600).ToUnixTimeSeconds(), @@ -335,7 +361,7 @@ public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCac cache.Set("testFunction.myfunc#b105f675a45bab746c0723da594d3b06", dr); // Act - DataRecord record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); + var record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); @@ -354,8 +380,8 @@ public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRe persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); - DateTimeOffset now = DateTimeOffset.UtcNow; - DataRecord dr = new DataRecord( + var now = DateTimeOffset.UtcNow; + var dr = new DataRecord( "testFunction.myfunc#b105f675a45bab746c0723da594d3b06", DataRecord.DataRecordStatus.COMPLETED, now.AddSeconds(-3).ToUnixTimeSeconds(), @@ -364,7 +390,7 @@ public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRe cache.Set("testFunction.myfunc#b105f675a45bab746c0723da594d3b06", dr); // Act - DataRecord record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); + var record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); @@ -387,7 +413,7 @@ public async Task GetRecord_WhenInvalidPayload_ShouldThrowValidationException() .Build(), "myfunc"); - DateTimeOffset now = DateTimeOffset.UtcNow; + var now = DateTimeOffset.UtcNow; // Act Func act = () => persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); @@ -443,11 +469,11 @@ public void GenerateHash_WhenInputIsString_ShouldGenerateMd5ofString() // Arrange var persistenceStore = new InMemoryPersistenceStore(); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); - string expectedHash = "70c24d88041893f7fbab4105b76fd9e1"; // MD5(Lambda rocks) + var expectedHash = "70c24d88041893f7fbab4105b76fd9e1"; // MD5(Lambda rocks) // Act var jsonValue = JsonValue.Create("Lambda rocks"); - string generatedHash = persistenceStore.GenerateHash( JsonDocument.Parse(jsonValue.ToJsonString()).RootElement); + var generatedHash = persistenceStore.GenerateHash(JsonDocument.Parse(jsonValue!.ToJsonString()).RootElement); // Assert generatedHash.Should().Be(expectedHash); @@ -459,11 +485,12 @@ public void GenerateHash_WhenInputIsObject_ShouldGenerateMd5ofJsonObject() // Arrange var persistenceStore = new InMemoryPersistenceStore(); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); - Product product = new Product(42, "Product", 12); - string expectedHash = "c83e720b399b3b4898c8734af177c53a"; // MD5({"Id":42,"Name":"Product","Price":12}) + var product = new Product(42, "Product", 12); + var expectedHash = "c83e720b399b3b4898c8734af177c53a"; // MD5({"Id":42,"Name":"Product","Price":12}) // Act - string generatedHash = persistenceStore.GenerateHash(JsonSerializer.SerializeToDocument(product)!.RootElement); + var jsonValue = JsonValue.Create(product); + var generatedHash = persistenceStore.GenerateHash(JsonDocument.Parse(jsonValue!.ToJsonString()).RootElement); // Assert generatedHash.Should().Be(expectedHash); @@ -475,7 +502,7 @@ public void GenerateHash_WhenInputIsDouble_ShouldGenerateMd5ofDouble() // Arrange var persistenceStore = new InMemoryPersistenceStore(); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null); - string expectedHash = "bb84c94278119c8838649706df4db42b"; // MD5(256.42) + var expectedHash = "bb84c94278119c8838649706df4db42b"; // MD5(256.42) // Act var generatedHash = persistenceStore.GenerateHash(JsonDocument.Parse("256.42").RootElement); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index 8f142b18..c43c2314 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -14,19 +14,16 @@ // */ // // using System; -// using System.Collections; // using System.Collections.Generic; // using System.Threading.Tasks; // using Amazon.DynamoDBv2; // using Amazon.DynamoDBv2.Model; +// using AWS.Lambda.Powertools.Common; // using AWS.Lambda.Powertools.Idempotency.Exceptions; -// using AWS.Lambda.Powertools.Idempotency.Internal; // using AWS.Lambda.Powertools.Idempotency.Persistence; // using FluentAssertions; // using Xunit; // -// [assembly: CollectionBehavior(DisableTestParallelization = true)] -// // namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; // // [Collection("Sequential")] From 877f082ace124f4945d81a5820e4d23b0fccf397 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Fri, 19 May 2023 17:49:17 +0100 Subject: [PATCH 18/32] remove constructor and skip e2e tests for CI --- .../Idempotency.cs | 13 +------------ .../IdempotencyTest.cs | 2 +- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 1df97fb1..63a7462e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -53,21 +53,10 @@ private void SetPersistenceStore(BasePersistenceStore persistenceStore) PersistenceStore = persistenceStore; } - private class Holder - { - // Explicit static constructor to tell C# compiler - // not to mark type as beforefieldinit - static Holder() - { - } - - internal static readonly Idempotency IdempotencyInstance = new Idempotency(PowertoolsConfigurations.Instance); - } - /// /// Holds the configuration for idempotency: /// - public static Idempotency Instance => Holder.IdempotencyInstance; + public static Idempotency Instance { get; } = new Idempotency(PowertoolsConfigurations.Instance); /// /// Use this method to configure persistence layer (mandatory) and idempotency options (optional) diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs index 802bd21b..2ac5cd76 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -31,7 +31,7 @@ public class IdempotencyTest { private const string TableName = "idempotency_table"; - [Fact] + [Fact(Skip = "Skip if running in CI")] public async Task EndToEndTest() { var client = new AmazonDynamoDBClient(); From 02590a4d9a1927c6dc00c0e44fb3a4a905d514ea Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Fri, 19 May 2023 18:26:46 +0100 Subject: [PATCH 19/32] update test packages. remove newtonsoft --- .../AWS.Lambda.Powertools.Idempotency.csproj | 5 ++--- .../Idempotency.cs | 13 ++++++------- .../AWS.Lambda.Powertools.Idempotency.Tests.csproj | 10 +++++----- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index 503546a2..684a6a50 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -31,9 +31,8 @@ - - - + + diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 63a7462e..80388245 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -54,14 +54,13 @@ private void SetPersistenceStore(BasePersistenceStore persistenceStore) } /// - /// Holds the configuration for idempotency: + /// Holds the idempotency Instance: /// public static Idempotency Instance { get; } = new Idempotency(PowertoolsConfigurations.Instance); /// /// Use this method to configure persistence layer (mandatory) and idempotency options (optional) /// - /// public static void Configure(Action configurationAction) { var builder = new IdempotencyBuilder(); @@ -96,7 +95,7 @@ public class IdempotencyBuilder /// Set the persistence layer to use for storing the request and response /// /// - /// + /// IdempotencyBuilder public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceStore) { _store = persistenceStore; @@ -107,7 +106,7 @@ public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceS /// Configure Idempotency to use DynamoDBPersistenceStore /// /// The builder being used to configure the - /// + /// IdempotencyBuilder public IdempotencyBuilder UseDynamoDb(Action builderAction) { var builder = @@ -121,7 +120,7 @@ public IdempotencyBuilder UseDynamoDb(Action bu /// Configure Idempotency to use DynamoDBPersistenceStore /// /// The DynamoDb table name - /// + /// IdempotencyBuilder public IdempotencyBuilder UseDynamoDb(string tableName) { var builder = @@ -134,7 +133,7 @@ public IdempotencyBuilder UseDynamoDb(string tableName) /// Set the idempotency configurations ///
/// The builder being used to configure the . - /// + /// IdempotencyBuilder public IdempotencyBuilder WithOptions(Action builderAction) { var builder = new IdempotencyOptionsBuilder(); @@ -147,7 +146,7 @@ public IdempotencyBuilder WithOptions(Action builderA /// Set the default idempotency configurations ///
/// - /// + /// IdempotencyBuilder public IdempotencyBuilder WithOptions(IdempotencyOptions options) { _options = options; diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj index 12a4cc58..c2627242 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj @@ -10,16 +10,16 @@ - + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 41a6f6f559c30341f30d0ba3da3d93c127064f86 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Mon, 22 May 2023 12:36:34 +0100 Subject: [PATCH 20/32] Upgrade Amazon.Lambda.APIGatewayEvents this changed the json so needs to update the MD5 Hash in tests --- .../Persistence/BasePersistenceStore.cs | 8 ++---- ...Lambda.Powertools.Idempotency.Tests.csproj | 2 +- .../Internal/LRUCacheTests.cs | 8 +++--- .../Persistence/BasePersistenceStoreTests.cs | 26 +++++++++---------- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index e21dce81..d83ab95d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -293,12 +293,8 @@ private string GetHashedIdempotencyKey(JsonDocument data) private bool IsMissingIdemPotencyKey(JsonElement data) { - return data.ValueKind == JsonValueKind.Null || data.ValueKind == JsonValueKind.Undefined; - // return (data == null) || - // (data is JsonArray && !data.) || - // (data.ValueKind == JsonValueKind.Array && !data.get) || - // (data.Type == JsonNodeType.String && data.ToString() == String.Empty) || - // (data.Type == JsonNodeType.Null); + return data.ValueKind == JsonValueKind.Null || data.ValueKind == JsonValueKind.Undefined + || (data.ValueKind == JsonValueKind.String && data.ToString() == string.Empty); } /// diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj index c2627242..d381afe4 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs index d3f9dfbe..d8af0649 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs @@ -76,10 +76,10 @@ public void TestDPMemorySmall() Assert.False(cache.TryGet(9998, out var result)); Assert.True(cache.TryGet(maxIdx - 1, out result)); - Assert.Equal(result, fib9999); + Assert.Equal(fib9999, result); Assert.True(cache.TryGet(maxIdx, out result)); - Assert.Equal(result, fib100000); + Assert.Equal(fib100000, result); } @@ -105,10 +105,10 @@ public void TestDPMemoryLarge() Assert.False(cache.TryGet(1, out var result)); Assert.True(cache.TryGet(maxIdx - 1, out result)); - Assert.Equal(result, fib9999); + Assert.Equal(fib9999, result); Assert.True(cache.TryGet(maxIdx, out result)); - Assert.Equal(result, fib100000); + Assert.Equal(fib100000, result); } [Fact] diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 5b7f4d79..3f945fe0 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -88,7 +88,7 @@ public async Task SaveInProgress_WhenDefaultConfig_ShouldSaveRecordInStore() dr.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); dr.ResponseData.Should().BeNull(); - dr.IdempotencyKey.Should().Be("testFunction#b105f675a45bab746c0723da594d3b06"); + dr.IdempotencyKey.Should().Be("testFunction#5eff007a9ed2789a9f9f6bc182fc6ae6"); dr.PayloadHash.Should().BeEmpty(); persistenceStore.Status.Should().Be(1); } @@ -280,7 +280,7 @@ public async Task SaveSuccess_WhenDefaultConfig_ShouldUpdateRecord() dr.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); dr.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); dr.ResponseData.Should().Be(JsonSerializer.Serialize(product)); - dr.IdempotencyKey.Should().Be("testFunction#b105f675a45bab746c0723da594d3b06"); + dr.IdempotencyKey.Should().Be("testFunction#5eff007a9ed2789a9f9f6bc182fc6ae6"); dr.PayloadHash.Should().BeEmpty(); persistenceStore.Status.Should().Be(2); cache.Count.Should().Be(0); @@ -307,12 +307,12 @@ public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() persistenceStore.Status.Should().Be(2); cache.Count.Should().Be(1); - var foundDataRecord = cache.TryGet("testFunction#b105f675a45bab746c0723da594d3b06", out var record); + var foundDataRecord = cache.TryGet("testFunction#5eff007a9ed2789a9f9f6bc182fc6ae6", out var record); foundDataRecord.Should().BeTrue(); record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); record.ExpiryTimestamp.Should().Be(now.AddSeconds(3600).ToUnixTimeSeconds()); record.ResponseData.Should().Be(JsonSerializer.Serialize(product)); - record.IdempotencyKey.Should().Be("testFunction#b105f675a45bab746c0723da594d3b06"); + record.IdempotencyKey.Should().Be("testFunction#5eff007a9ed2789a9f9f6bc182fc6ae6"); record.PayloadHash.Should().BeEmpty(); } @@ -334,7 +334,7 @@ public async Task GetRecord_WhenRecordIsInStore_ShouldReturnRecordFromPersistenc var record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert - record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); + record.IdempotencyKey.Should().Be("testFunction.myfunc#5eff007a9ed2789a9f9f6bc182fc6ae6"); record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); record.ResponseData.Should().Be("Response"); persistenceStore.Status.Should().Be(0); @@ -353,18 +353,18 @@ public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCac var now = DateTimeOffset.UtcNow; var dr = new DataRecord( - "testFunction.myfunc#b105f675a45bab746c0723da594d3b06", + "testFunction.myfunc#5eff007a9ed2789a9f9f6bc182fc6ae6", DataRecord.DataRecordStatus.COMPLETED, now.AddSeconds(3600).ToUnixTimeSeconds(), "result of the function", null); - cache.Set("testFunction.myfunc#b105f675a45bab746c0723da594d3b06", dr); + cache.Set("testFunction.myfunc#5eff007a9ed2789a9f9f6bc182fc6ae6", dr); // Act var record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert - record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); + record.IdempotencyKey.Should().Be("testFunction.myfunc#5eff007a9ed2789a9f9f6bc182fc6ae6"); record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); record.ResponseData.Should().Be("result of the function"); persistenceStore.Status.Should().Be(-1); @@ -382,18 +382,18 @@ public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRe var now = DateTimeOffset.UtcNow; var dr = new DataRecord( - "testFunction.myfunc#b105f675a45bab746c0723da594d3b06", + "testFunction.myfunc#5eff007a9ed2789a9f9f6bc182fc6ae6", DataRecord.DataRecordStatus.COMPLETED, now.AddSeconds(-3).ToUnixTimeSeconds(), "result of the function", null); - cache.Set("testFunction.myfunc#b105f675a45bab746c0723da594d3b06", dr); + cache.Set("testFunction.myfunc#5eff007a9ed2789a9f9f6bc182fc6ae6", dr); // Act var record = await persistenceStore.GetRecord(JsonSerializer.SerializeToDocument(request)!, now); // Assert - record.IdempotencyKey.Should().Be("testFunction.myfunc#b105f675a45bab746c0723da594d3b06"); + record.IdempotencyKey.Should().Be("testFunction.myfunc#5eff007a9ed2789a9f9f6bc182fc6ae6"); record.Status.Should().Be(DataRecord.DataRecordStatus.INPROGRESS); record.ResponseData.Should().Be("Response"); persistenceStore.Status.Should().Be(0); @@ -449,8 +449,8 @@ public async Task DeleteRecord_WhenLocalCacheEnabled_ShouldDeleteRecordFromCache persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), null, cache); - cache.Set("testFunction#b105f675a45bab746c0723da594d3b06", - new DataRecord("testFunction#b105f675a45bab746c0723da594d3b06", + cache.Set("testFunction#5eff007a9ed2789a9f9f6bc182fc6ae6", + new DataRecord("testFunction#5eff007a9ed2789a9f9f6bc182fc6ae6", DataRecord.DataRecordStatus.COMPLETED, 123, null, null)); From dd6521fe9b28b3c8d222170cededf441bb6b34ff Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Wed, 24 May 2023 10:51:31 +0100 Subject: [PATCH 21/32] move constants. Enable integration tests dynamo. Code inspection fixes --- .../Core/Constants.cs | 10 + .../Core/PowertoolsLambdaContext.cs | 2 - .../Idempotency.cs | 12 +- .../IdempotencyOptionsBuilder.cs | 12 +- .../IdempotentAttribute.cs | 1 - .../Internal/Constants.cs | 30 - .../Internal/IdempotencyAspectHandler.cs | 52 +- .../Internal/LRUCache.cs | 43 +- .../Output/ConsoleLog.cs | 2 +- .../Persistence/BasePersistenceStore.cs | 5 +- .../Persistence/DataRecord.cs | 4 +- .../Persistence/DynamoDBPersistenceStore.cs | 54 +- .../Serialization/JsonFunction.cs | 5 +- .../Handlers/IdempotencyEnabledFunction.cs | 2 +- .../Handlers/IdempotencyFunction.cs | 5 +- .../IdempotencyTest.cs | 5 +- .../IntegrationTestBase.cs | 6 +- .../Internal/LRUCacheTests.cs | 11 +- .../Model/Basket.cs | 6 +- .../Model/Product.cs | 5 +- .../Persistence/BasePersistenceStoreTests.cs | 22 +- .../DynamoDBPersistenceStoreTests.cs | 709 +++++++++--------- 22 files changed, 499 insertions(+), 504 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs index c586d563..80762d98 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs @@ -95,4 +95,14 @@ internal static class Constants /// Constant for IDEMPOTENCY_DISABLED_ENV environment variable /// internal const string IdempotencyDisabledEnv = "POWERTOOLS_IDEMPOTENCY_DISABLED"; + + /// + /// Constant for AWS_REGION_ENV environment variable + /// + internal const string AwsRegionEnv = "AWS_REGION"; + + /// + /// Constant for LAMBDA_FUNCTION_NAME_ENV environment variable + /// + internal const string LambdaFunctionNameEnv = "AWS_LAMBDA_FUNCTION_NAME"; } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs index b07013a8..7f9b3fee 100644 --- a/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs +++ b/libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsLambdaContext.cs @@ -1,6 +1,4 @@ using System; -using System.Linq; -using System.Reflection; namespace AWS.Lambda.Powertools.Common; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 80388245..2d4cda50 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -56,7 +56,7 @@ private void SetPersistenceStore(BasePersistenceStore persistenceStore) /// /// Holds the idempotency Instance: /// - public static Idempotency Instance { get; } = new Idempotency(PowertoolsConfigurations.Instance); + public static Idempotency Instance { get; } = new(PowertoolsConfigurations.Instance); /// /// Use this method to configure persistence layer (mandatory) and idempotency options (optional) @@ -69,14 +69,8 @@ public static void Configure(Action configurationAction) { throw new NullReferenceException("Persistence Layer is null, configure one with 'WithPersistenceStore()'"); } - if (builder.Options != null) - { - Instance.SetConfig(builder.Options); - } - else - { - Instance.SetConfig(new IdempotencyOptionsBuilder().Build()); - } + + Instance.SetConfig(builder.Options ?? new IdempotencyOptionsBuilder().Build()); Instance.SetPersistenceStore(builder.Store); } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs index 181044bc..0cda2fb9 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs @@ -8,12 +8,12 @@ namespace AWS.Lambda.Powertools.Idempotency; /// public class IdempotencyOptionsBuilder { - private int _localCacheMaxItems = 256; - private bool _useLocalCache = false; + private readonly int _localCacheMaxItems = 256; + private bool _useLocalCache; private long _expirationInSeconds = 60 * 60; // 1 hour - private string _eventKeyJmesPath = null; - private string _payloadValidationJmesPath = null; - private bool _throwOnNoIdempotencyKey = false; + private string _eventKeyJmesPath; + private string _payloadValidationJmesPath; + private bool _throwOnNoIdempotencyKey; private string _hashFunction = "MD5"; private ILog _log = new NullLog(); @@ -26,7 +26,7 @@ public class IdempotencyOptionsBuilder /// /// an instance of IdempotencyConfig public IdempotencyOptions Build() => - new IdempotencyOptions(_eventKeyJmesPath, + new(_eventKeyJmesPath, _payloadValidationJmesPath, _throwOnNoIdempotencyKey, _useLocalCache, diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index e308e2a5..882fa87c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -20,7 +20,6 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; -using Constants = AWS.Lambda.Powertools.Common.Constants; namespace AWS.Lambda.Powertools.Idempotency; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs deleted file mode 100644 index 7e757032..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/Constants.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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. - */ - -namespace AWS.Lambda.Powertools.Idempotency.Internal; - -/// -/// Class Constants -/// -internal class Constants { - /// - /// Constant for LAMBDA_FUNCTION_NAME_ENV environment variable - /// - internal const string LambdaFunctionNameEnv = "AWS_LAMBDA_FUNCTION_NAME"; - /// - /// Constant for AWS_REGION_ENV environment variable - /// - internal const string AwsRegionEnv = "AWS_REGION"; -} diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs index eaa12860..7901c80d 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs @@ -20,7 +20,6 @@ using AWS.Lambda.Powertools.Idempotency.Output; using AWS.Lambda.Powertools.Idempotency.Persistence; - namespace AWS.Lambda.Powertools.Idempotency.Internal; internal class IdempotencyAspectHandler @@ -56,7 +55,7 @@ public async Task Handle() // IdempotencyInconsistentStateException can happen under rare but expected cases // when persistent state changes in the small time between put & get requests. // In most cases we can retry successfully on this exception. - for (var i = 0; true; i++) + for (var i = 0;; i++) { try { @@ -151,31 +150,30 @@ private Task GetIdempotencyRecord() /// private Task HandleForStatus(DataRecord record) { - // This code path will only be triggered if the record becomes expired between the saveInProgress call and here - if (DataRecord.DataRecordStatus.EXPIRED == record.Status) - { - throw new IdempotencyInconsistentStateException("saveInProgress and getRecord return inconsistent results"); - } - - if (DataRecord.DataRecordStatus.INPROGRESS == record.Status) - { - throw new IdempotencyAlreadyInProgressException("Execution already in progress with idempotency key: " + - record.IdempotencyKey); - } - - try - { - _log.WriteDebug("Response for key '{0}' retrieved from idempotency store, skipping the function", record.IdempotencyKey); - var result = JsonSerializer.Deserialize(record.ResponseData!); - if (result is null) - { - throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name); - } - return Task.FromResult(result); - } - catch (Exception e) - { - throw new IdempotencyPersistenceLayerException("Unable to get function response as " + typeof(T).Name, e); + switch (record.Status) + { + // This code path will only be triggered if the record becomes expired between the saveInProgress call and here + case DataRecord.DataRecordStatus.EXPIRED: + throw new IdempotencyInconsistentStateException("saveInProgress and getRecord return inconsistent results"); + case DataRecord.DataRecordStatus.INPROGRESS: + throw new IdempotencyAlreadyInProgressException("Execution already in progress with idempotency key: " + + record.IdempotencyKey); + case DataRecord.DataRecordStatus.COMPLETED: + default: + try + { + _log.WriteDebug("Response for key '{0}' retrieved from idempotency store, skipping the function", record.IdempotencyKey); + var result = JsonSerializer.Deserialize(record.ResponseData!); + if (result is null) + { + throw new IdempotencyPersistenceLayerException("Unable to cast function response as " + typeof(T).Name); + } + return Task.FromResult(result); + } + catch (Exception e) + { + throw new IdempotencyPersistenceLayerException("Unable to get function response as " + typeof(T).Name, e); + } } } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs index 88d1e86c..741418ce 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs @@ -18,7 +18,7 @@ internal sealed class LRUCache /// private const int DefaultCapacity = 255; - private readonly object _lockObj = new object(); + private readonly object _lockObj = new(); private readonly int _capacity; private readonly Dictionary _cacheMap; private readonly LinkedList _cacheList; @@ -62,7 +62,7 @@ public bool TryGet(TKey key, out TValue value) } } - value = default(TValue); + value = default; return false; } @@ -81,9 +81,16 @@ public void Set(TKey key, TValue value) if (_cacheMap.Count >= _capacity) { node = _cacheList.Last; - _cacheMap.Remove(node.Value); - _cacheList.RemoveLast(); - node.Value = key; + if (node != null) + { + _cacheMap.Remove(node.Value); + _cacheList.RemoveLast(); + node.Value = key; + } + else + { + node = new LinkedListNode(key); + } } else { @@ -111,26 +118,38 @@ public void Delete(TKey key) } } - public int Count => _cacheList.Count; + public int Count + { + get + { + lock (_lockObj) + { + return _cacheList.Count; + } + } + } private void Touch(LinkedListNode node) { - if (node != _cacheList.First) + lock (_lockObj) { - _cacheList.Remove(node); - _cacheList.AddFirst(node); + if (node != _cacheList.First) + { + _cacheList.Remove(node); + _cacheList.AddFirst(node); + } } } private struct Entry { - public LinkedListNode Node; + public readonly LinkedListNode Node; public TValue Value; public Entry(LinkedListNode node, TValue value) { - this.Node = node; - this.Value = value; + Node = node; + Value = value; } } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs index 2745a35b..3c63e0aa 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs @@ -50,7 +50,7 @@ public class ConsoleLog : ILog /// The args. public void WriteDebug(string format, params object[] args) => Write(ConsoleColor.Cyan, format, args); - static void Write(ConsoleColor color, string format, object[] args) + private static void Write(ConsoleColor color, string format, object[] args) { var oldColor = Console.ForegroundColor; Console.ForegroundColor = color; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index d83ab95d..4a4420e6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -18,6 +18,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; using AWS.Lambda.Powertools.Idempotency.Output; @@ -29,7 +30,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; /// /// Persistence layer that will store the idempotency result. /// Base implementation. See for an implementation (default one) -/// Extends this class to use your own implementation (DocumentDB, Elasticache, ...) +/// Extend this class to use your own implementation (DocumentDB, Elasticache, ...) /// public abstract class BasePersistenceStore : IPersistenceStore { @@ -291,7 +292,7 @@ private string GetHashedIdempotencyKey(JsonDocument data) return _functionName + "#" + hash; } - private bool IsMissingIdemPotencyKey(JsonElement data) + private static bool IsMissingIdemPotencyKey(JsonElement data) { return data.ValueKind == JsonValueKind.Null || data.ValueKind == JsonValueKind.Undefined || (data.ValueKind == JsonValueKind.String && data.ToString() == string.Empty); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs index 2144bc50..8f98fd35 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs @@ -96,7 +96,7 @@ public DataRecordStatus Status /// /// The DataRecord to compare with the current object. /// true if the specified DataRecord is equal to the current DataRecord; otherwise, false. - protected bool Equals(DataRecord other) + private bool Equals(DataRecord other) { return _status == other._status && IdempotencyKey == other.IdempotencyKey @@ -110,7 +110,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (obj.GetType() != GetType()) return false; return Equals((DataRecord) obj); } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index 88204262..3b4c8b14 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -21,7 +21,6 @@ using Amazon.DynamoDBv2.Model; using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; -using Constants = AWS.Lambda.Powertools.Idempotency.Internal.Constants; namespace AWS.Lambda.Powertools.Idempotency.Persistence; @@ -64,19 +63,22 @@ internal DynamoDBPersistenceStore(string tableName, { _dynamoDbClient = client; } - else + else { - if (PowertoolsConfigurations.Instance.IdempotencyDisabled) - { + var isIdempotencyDisabled = bool.TryParse(Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv), out var result) && result; + + if (isIdempotencyDisabled) + { + // we do not want to create a DynamoDbClient if idempotency is disabled + // null is ok as idempotency won't be called + _dynamoDbClient = null; + + } else { var clientConfig = new AmazonDynamoDBConfig { RegionEndpoint = RegionEndpoint.GetBySystemName(Environment.GetEnvironmentVariable(Constants.AwsRegionEnv)) }; _dynamoDbClient = new AmazonDynamoDBClient(clientConfig); - } else { - // we do not want to create a DynamoDbClient if idempotency is disabled - // null is ok as idempotency won't be called - _dynamoDbClient = null; } } } @@ -85,7 +87,7 @@ internal DynamoDBPersistenceStore(string tableName, /// public override async Task GetRecord(string idempotencyKey) { - var getItemRequest = new GetItemRequest() + var getItemRequest = new GetItemRequest { TableName = _tableName, ConsistentRead = true, @@ -108,30 +110,30 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) Dictionary item = new(GetKey(record.IdempotencyKey)) { { - this._expiryAttr, new AttributeValue() + _expiryAttr, new AttributeValue { N = record.ExpiryTimestamp.ToString() } }, - { this._statusAttr, new AttributeValue(record.Status.ToString()) } + { _statusAttr, new AttributeValue(record.Status.ToString()) } }; if (PayloadValidationEnabled) { - item.Add(this._validationAttr, new AttributeValue(record.PayloadHash)); + item.Add(_validationAttr, new AttributeValue(record.PayloadHash)); } try { Log.WriteDebug("Putting record for idempotency key: {0}", record.IdempotencyKey); - var expressionAttributeNames = new Dictionary + var expressionAttributeNames = new Dictionary { - {"#id", this._keyAttr}, - {"#expiry", this._expiryAttr} + {"#id", _keyAttr}, + {"#expiry", _expiryAttr} }; - var request = new PutItemRequest() + var request = new PutItemRequest { TableName = _tableName, Item = item, @@ -139,7 +141,7 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) ExpressionAttributeNames = expressionAttributeNames, ExpressionAttributeValues = new Dictionary { - {":now", new AttributeValue() {N = now.ToUnixTimeSeconds().ToString()}} + {":now", new AttributeValue {N = now.ToUnixTimeSeconds().ToString()}} } }; await _dynamoDbClient!.PutItemAsync(request); @@ -161,22 +163,22 @@ public override async Task UpdateRecord(DataRecord record) var expressionAttributeNames = new Dictionary { - {"#response_data", this._dataAttr}, - {"#expiry", this._expiryAttr}, - {"#status", this._statusAttr} + {"#response_data", _dataAttr}, + {"#expiry", _expiryAttr}, + {"#status", _statusAttr} }; var expressionAttributeValues = new Dictionary { {":response_data", new AttributeValue(record.ResponseData)}, - {":expiry", new AttributeValue(){N=record.ExpiryTimestamp.ToString()}}, + {":expiry", new AttributeValue {N=record.ExpiryTimestamp.ToString()}}, {":status", new AttributeValue(record.Status.ToString())} }; if (PayloadValidationEnabled) { updateExpression += ", #validation_key = :validation_key"; - expressionAttributeNames.Add("#validation_key", this._validationAttr); + expressionAttributeNames.Add("#validation_key", _validationAttr); expressionAttributeValues.Add(":validation_key", new AttributeValue(record.PayloadHash)); } @@ -208,7 +210,7 @@ public override async Task DeleteRecord(string idempotencyKey) /// /// item Item from dynamodb response /// DataRecord instance - private DataRecord ItemToRecord(Dictionary item) + private DataRecord ItemToRecord(Dictionary item) { // data and validation payload may be null var hasDataAttribute = item.TryGetValue(_dataAttr, out var data); @@ -228,10 +230,10 @@ private DataRecord ItemToRecord(Dictionary item) /// private Dictionary GetKey(string idempotencyKey) { - Dictionary key = new(); + Dictionary key = new(); if (_sortKeyAttr != null) { - key[_keyAttr] = new AttributeValue(this._staticPkValue); + key[_keyAttr] = new AttributeValue(_staticPkValue); key[_sortKeyAttr] = new AttributeValue(idempotencyKey); } else @@ -257,7 +259,7 @@ public class DynamoDBPersistenceStoreBuilder private string _tableName = null!; private string _keyAttr = "id"; - private string _staticPkValue = string.Format("idempotency#%s", FuncEnv ?? ""); + private string _staticPkValue = $"idempotency#{FuncEnv}"; private string _sortKeyAttr; private string _expiryAttr = "expiration"; private string _statusAttr = "status"; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs index f4be86e7..94ed3f25 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Serialization/JsonFunction.cs @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +using System.Diagnostics; using DevLab.JmesPath.Functions; using Newtonsoft.Json.Linq; @@ -32,8 +33,8 @@ public JsonFunction() /// public override JToken Execute(params JmesPathFunctionArgument[] args) { - System.Diagnostics.Debug.Assert(args.Length == 1); - System.Diagnostics.Debug.Assert(args[0].IsToken); + Debug.Assert(args.Length == 1); + Debug.Assert(args[0].IsToken); var argument = args[0]; var token = argument.Token; return JToken.Parse(token.ToString()); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs index cd88eba8..d18be44d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs @@ -21,7 +21,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; public class IdempotencyEnabledFunction { - public bool HandlerExecuted = false; + public bool HandlerExecuted; [Idempotent] public Task Handle(Product input, ILambdaContext context) diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs index 8925b089..832eb95a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs @@ -18,10 +18,7 @@ using System.IO; using System.Net.Http; using System.Text.Json; -using System.Text.Json.Nodes; -using System.Threading; using System.Threading.Tasks; -using Amazon; using Amazon.DynamoDBv2; using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.Core; @@ -30,7 +27,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; public class IdempotencyFunction { - public bool HandlerExecuted = false; + public bool HandlerExecuted; public IdempotencyFunction(AmazonDynamoDBClient client) { diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs index 2ac5cd76..5cb1c801 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -13,7 +13,6 @@ * permissions and limitations under the License. */ -using System; using System.IO; using System.Text.Json; using System.Threading.Tasks; @@ -31,7 +30,9 @@ public class IdempotencyTest { private const string TableName = "idempotency_table"; - [Fact(Skip = "Skip if running in CI")] + //[Fact(Skip = "Integration Tests - Require setup")] + [Trait("Category", "Integration")] + [Fact] public async Task EndToEndTest() { var client = new AmazonDynamoDBClient(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs index 31e074bd..50e3cd27 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs @@ -35,7 +35,7 @@ public async Task InitializeAsync() // initialize TestContainers or Ductus.FluentDocker or have DynamoDb local docker running var credentials = new BasicAWSCredentials("FAKE", "FAKE"); - var amazonDynamoDbConfig = new AmazonDynamoDBConfig() + var amazonDynamoDbConfig = new AmazonDynamoDBConfig { ServiceURL = new UriBuilder("http", "localhost", 8000).Uri.ToString(), AuthenticationRegion = "us-east-1" @@ -45,11 +45,11 @@ public async Task InitializeAsync() var createTableRequest = new CreateTableRequest { TableName = TableName, - KeySchema = new List() + KeySchema = new List { new("id", KeyType.HASH) }, - AttributeDefinitions = new List() + AttributeDefinitions = new List { new() { diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs index d8af0649..242e4522 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs @@ -123,14 +123,15 @@ public async Task TestMultiThreadingAsync() const int numOfOps = 1000; for (var i = 0; i < numOfThreads; i++) { - tasks.Add(Task.Run(() => StoreElement(cache, numOfOps, i))); + var idx = i; + tasks.Add(Task.Run(() => StoreElement(cache, numOfOps))); } await Task.WhenAll(tasks).ConfigureAwait(false); for (var i = numOfOps - numOfThreads; i < numOfOps; i++) { - Assert.True(cache.TryGet(i, out var result)); + Assert.True(cache.TryGet(i, out _)); } } @@ -155,13 +156,11 @@ public void TestDelete() Assert.Equal("num3", result); } - private void StoreElement(LRUCache cache, int numOfOps, int idx) + private static void StoreElement(LRUCache cache, int numOfOps) { for (var i = 0; i < numOfOps; i++) { - var key = i; - var value = i; - cache.Set(key, value); + cache.Set(i, i); } } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs index 919d3b22..4b0b8d51 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Basket.cs @@ -20,7 +20,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Model; public class Basket { - public List Products { get; set; } = new List(); + public List Products { get; } = new(); public Basket() { @@ -36,7 +36,7 @@ public void Add(Product product) Products.Add(product); } - protected bool Equals(Basket other) + private bool Equals(Basket other) { return Products.All(other.Products.Contains); } @@ -45,7 +45,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (obj.GetType() != GetType()) return false; return Equals((Basket) obj); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs index e1bd54e1..eb7d4657 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Model/Product.cs @@ -19,8 +19,11 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Model; public class Product : IEquatable { + // ReSharper disable once MemberCanBePrivate.Global public long Id { get; } + // ReSharper disable once MemberCanBePrivate.Global public string Name { get; } + // ReSharper disable once MemberCanBePrivate.Global public double Price { get; } public Product(long id, string name, double price) @@ -43,7 +46,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (obj.GetType() != GetType()) return false; return Equals((Product) obj); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 3f945fe0..75281f4b 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -30,10 +30,10 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; public class BasePersistenceStoreTests { - class InMemoryPersistenceStore : BasePersistenceStore + private class InMemoryPersistenceStore : BasePersistenceStore { - private string _validationHash = null; - public DataRecord DataRecord = null; + private readonly string _validationHash = null; + public DataRecord DataRecord; public int Status = -1; public override Task GetRecord(string idempotencyKey) { @@ -199,7 +199,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSet_AndNotExpired_ShouldThrowEx var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new ((int) 2); + LRUCache cache = new (2); persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true) .WithEventKeyJmesPath("powertools_json(Body).id") @@ -231,7 +231,7 @@ public async Task SaveInProgress_WhenLocalCacheIsSetButExpired_ShouldRemoveFromC var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new ((int) 2); + LRUCache cache = new (2); persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithEventKeyJmesPath("powertools_json(Body).id") .WithUseLocalCache(true) @@ -265,7 +265,7 @@ public async Task SaveSuccess_WhenDefaultConfig_ShouldUpdateRecord() // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new ((int) 2); + LRUCache cache = new (2); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), null, cache); var product = new Product(34543, "product", 42); @@ -292,7 +292,7 @@ public async Task SaveSuccess_WhenCacheEnabled_ShouldSaveInCache() // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new ((int) 2); + LRUCache cache = new (2); persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), null, cache); @@ -325,7 +325,7 @@ public async Task GetRecord_WhenRecordIsInStore_ShouldReturnRecordFromPersistenc var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new((int) 2); + LRUCache cache = new(2); persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(), "myfunc", cache); var now = DateTimeOffset.UtcNow; @@ -346,7 +346,7 @@ public async Task GetRecord_WhenCacheEnabledNotExpired_ShouldReturnRecordFromCac // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new((int) 2); + LRUCache cache = new(2); persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); @@ -376,7 +376,7 @@ public async Task GetRecord_WhenLocalCacheEnabledButRecordExpired_ShouldReturnRe // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new((int) 2); + LRUCache cache = new(2); persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), "myfunc", cache); @@ -445,7 +445,7 @@ public async Task DeleteRecord_WhenLocalCacheEnabled_ShouldDeleteRecordFromCache // Arrange var persistenceStore = new InMemoryPersistenceStore(); var request = LoadApiGatewayProxyRequest(); - LRUCache cache = new ((int) 2); + LRUCache cache = new (2); persistenceStore.Configure(new IdempotencyOptionsBuilder() .WithUseLocalCache(true).Build(), null, cache); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index c43c2314..7b6a4142 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -1,353 +1,356 @@ -// /* -// * 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.Collections.Generic; -// using System.Threading.Tasks; -// using Amazon.DynamoDBv2; -// using Amazon.DynamoDBv2.Model; -// using AWS.Lambda.Powertools.Common; -// using AWS.Lambda.Powertools.Idempotency.Exceptions; -// using AWS.Lambda.Powertools.Idempotency.Persistence; -// using FluentAssertions; -// using Xunit; -// -// namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; -// -// [Collection("Sequential")] -// public class DynamoDBPersistenceStoreTests : IntegrationTestBase -// { -// //putRecord -// [Fact] -// public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB() -// { -// // Arrange -// DateTimeOffset now = DateTimeOffset.UtcNow; -// long expiry = now.AddSeconds(3600).ToUnixTimeSeconds(); -// var key = CreateKey("key"); -// -// // Act -// await _dynamoDbPersistenceStore -// .PutRecord(new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, null, null), now); -// -// // Assert -// var getItemResponse = -// await client.GetItemAsync(new GetItemRequest -// { -// TableName = TABLE_NAME, -// Key = key -// }); -// -// var item = getItemResponse.Item; -// item.Should().NotBeNull(); -// item["status"].S.Should().Be("COMPLETED"); -// item["expiration"].N.Should().Be(expiry.ToString()); -// } -// -// [Fact] -// public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlreadyExistsException() -// { -// // Arrange -// var key = CreateKey("key"); -// -// // Insert a fake item with same id -// Dictionary item = new(key); -// DateTimeOffset now = DateTimeOffset.UtcNow; -// long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); -// item.Add("expiration", new AttributeValue(){N = expiry.ToString()}); -// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); -// item.Add("data", new AttributeValue("Fake Data")); -// await client.PutItemAsync(new PutItemRequest -// { -// TableName = TABLE_NAME, -// Item = item -// }); -// long expiry2 = now.AddSeconds(3600).ToUnixTimeSeconds(); -// -// // Act -// Func act = () => _dynamoDbPersistenceStore.PutRecord( -// new DataRecord("key", -// DataRecord.DataRecordStatus.INPROGRESS, -// expiry2, -// null, -// null -// ), now); -// -// // Assert -// await act.Should().ThrowAsync(); -// -// // item was not updated, retrieve the initial one -// Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest -// { -// TableName = TABLE_NAME, -// Key = key -// })).Item; -// itemInDb.Should().NotBeNull(); -// itemInDb["status"].S.Should().Be("COMPLETED"); -// itemInDb["expiration"].N.Should().Be(expiry.ToString()); -// itemInDb["data"].S.Should().Be("Fake Data"); -// } -// -// //getRecord -// [Fact] -// public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecord() -// { -// // Arrange -// //await InitializeAsync(); -// -// // Insert a fake item with same id -// Dictionary item = new() -// { -// {"id", new AttributeValue("key")} //key -// }; -// DateTimeOffset now = DateTimeOffset.UtcNow; -// long expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); -// item.Add("expiration", new AttributeValue -// { -// N = expiry.ToString() -// }); -// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); -// item.Add("data", new AttributeValue("Fake Data")); -// var response = await client.PutItemAsync(new PutItemRequest() -// { -// TableName = TABLE_NAME, -// Item = item -// }); -// -// // Act -// DataRecord record = await _dynamoDbPersistenceStore.GetRecord("key"); -// -// // Assert -// record.IdempotencyKey.Should().Be("key"); -// record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); -// record.ResponseData.Should().Be("Fake Data"); -// record.ExpiryTimestamp.Should().Be(expiry); -// } -// -// [Fact] -// public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() -// { -// // Act -// Func act = () => _dynamoDbPersistenceStore.GetRecord("key"); -// -// // Assert -// await act.Should().ThrowAsync(); -// } -// //updateRecord -// -// [Fact] -// public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() -// { -// // Arrange: Insert a fake item with same id -// var key = CreateKey("key"); -// Dictionary item = new(key); -// DateTimeOffset now = DateTimeOffset.UtcNow; -// long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); -// item.Add("expiration", new AttributeValue -// { -// N = expiry.ToString() -// }); -// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); -// await client.PutItemAsync(new PutItemRequest -// { -// TableName = TABLE_NAME, -// Item = item -// }); -// // enable payload validation -// _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().WithPayloadValidationJmesPath("path").Build(), -// null); -// -// // Act -// expiry = now.AddSeconds(3600).ToUnixTimeMilliseconds(); -// DataRecord record = new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, "Fake result", "hash"); -// await _dynamoDbPersistenceStore.UpdateRecord(record); -// -// // Assert -// Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest -// { -// TableName = TABLE_NAME, -// Key = key -// })).Item; -// -// itemInDb["status"].S.Should().Be("COMPLETED"); -// itemInDb["expiration"].N.Should().Be(expiry.ToString()); -// itemInDb["data"].S.Should().Be("Fake result"); -// itemInDb["validation"].S.Should().Be("hash"); -// } -// -// //deleteRecord -// [Fact] -// public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() -// { -// // Arrange: Insert a fake item with same id -// var key = CreateKey("key"); -// Dictionary item = new(key); -// DateTimeOffset now = DateTimeOffset.UtcNow; -// long expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); -// item.Add("expiration", new AttributeValue(){N=expiry.ToString()}); -// item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); -// await client.PutItemAsync(new PutItemRequest -// { -// TableName = TABLE_NAME, -// Item = item -// }); -// var scanResponse = await client.ScanAsync(new ScanRequest -// { -// TableName = TABLE_NAME -// }); -// scanResponse.Items.Count.Should().Be(1); -// -// // Act -// await _dynamoDbPersistenceStore.DeleteRecord("key"); -// -// // Assert -// scanResponse = await client.ScanAsync(new ScanRequest -// { -// TableName = TABLE_NAME -// }); -// scanResponse.Items.Count.Should().Be(0); -// } -// -// [Fact] -// public async Task EndToEndWithCustomAttrNamesAndSortKey() -// { -// var TABLE_NAME_CUSTOM = "idempotency_table_custom"; -// try -// { -// var createTableRequest = new CreateTableRequest -// { -// TableName = TABLE_NAME_CUSTOM, -// KeySchema = new List() -// { -// new KeySchemaElement("key", KeyType.HASH), -// new KeySchemaElement("sortkey", KeyType.RANGE) -// }, -// AttributeDefinitions = new List() -// { -// new AttributeDefinition("key", ScalarAttributeType.S), -// new AttributeDefinition("sortkey", ScalarAttributeType.S) -// }, -// BillingMode = BillingMode.PAY_PER_REQUEST -// }; -// await client.CreateTableAsync(createTableRequest); -// DynamoDBPersistenceStore persistenceStore = new DynamoDBPersistenceStoreBuilder() -// .WithTableName(TABLE_NAME_CUSTOM) -// .WithDynamoDBClient(client) -// .WithDataAttr("result") -// .WithExpiryAttr("expiry") -// .WithKeyAttr("key") -// .WithSortKeyAttr("sortkey") -// .WithStaticPkValue("pk") -// .WithStatusAttr("state") -// .WithValidationAttr("valid") -// .Build(); -// persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); -// -// DateTimeOffset now = DateTimeOffset.UtcNow; -// DataRecord record = new DataRecord( -// "mykey", -// DataRecord.DataRecordStatus.INPROGRESS, -// now.AddSeconds(400).ToUnixTimeMilliseconds(), -// null, -// null -// ); -// // PUT -// await persistenceStore.PutRecord(record, now); -// -// Dictionary customKey = new(); -// customKey.Add("key", new AttributeValue("pk")); -// customKey.Add("sortkey", new AttributeValue("mykey")); -// -// Dictionary itemInDb = (await client.GetItemAsync(new GetItemRequest -// { -// TableName = TABLE_NAME_CUSTOM, -// Key = customKey -// })).Item; -// -// // GET -// DataRecord recordInDb = await persistenceStore.GetRecord("mykey"); -// -// itemInDb.Should().NotBeNull(); -// itemInDb["key"].S.Should().Be("pk"); -// itemInDb["sortkey"].S.Should().Be(recordInDb.IdempotencyKey); -// itemInDb["state"].S.Should().Be(recordInDb.Status.ToString()); -// itemInDb["expiry"].N.Should().Be(recordInDb.ExpiryTimestamp.ToString()); -// -// // UPDATE -// DataRecord updatedRecord = new DataRecord( -// "mykey", -// DataRecord.DataRecordStatus.COMPLETED, -// now.AddSeconds(500).ToUnixTimeMilliseconds(), -// "response", -// null -// ); -// await persistenceStore.UpdateRecord(updatedRecord); -// recordInDb = await persistenceStore.GetRecord("mykey"); -// recordInDb.Should().Be(updatedRecord); -// -// // DELETE -// await persistenceStore.DeleteRecord("mykey"); -// (await client.ScanAsync(new ScanRequest -// { -// TableName = TABLE_NAME_CUSTOM -// })).Count.Should().Be(0); -// -// } -// finally -// { -// try -// { -// await client.DeleteTableAsync(new DeleteTableRequest -// { -// TableName = TABLE_NAME_CUSTOM -// }); -// } -// catch (Exception) -// { -// // OK -// } -// } -// } -// -// [Fact] -// public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() -// { -// try -// { -// // Arrange -// Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); -// -// DynamoDBPersistenceStore store = new DynamoDBPersistenceStoreBuilder().WithTableName(TABLE_NAME).Build(); -// -// // Act -// Func act = () => store.GetRecord("fake"); -// -// // Assert -// await act.Should().ThrowAsync(); -// } -// finally -// { -// Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); -// } -// } -// private static Dictionary CreateKey(string keyValue) -// { -// var key = new Dictionary() -// { -// {"id", new AttributeValue(keyValue)} -// }; -// return key; -// } -// } \ No newline at end of file +/* + * 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.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using AWS.Lambda.Powertools.Common; +using AWS.Lambda.Powertools.Idempotency.Exceptions; +using AWS.Lambda.Powertools.Idempotency.Persistence; +using FluentAssertions; +using Xunit; + +namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; + +[Collection("Sequential")] +[Trait("Category", "Integration")] +public class DynamoDbPersistenceStoreTests : IntegrationTestBase +{ + //putRecord + [Fact] + public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var expiry = now.AddSeconds(3600).ToUnixTimeSeconds(); + var key = CreateKey("key"); + + // Act + await DynamoDbPersistenceStore + .PutRecord(new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, null, null), now); + + // Assert + var getItemResponse = + await Client.GetItemAsync(new GetItemRequest + { + TableName = TableName, + Key = key + }); + + var item = getItemResponse.Item; + item.Should().NotBeNull(); + item["status"].S.Should().Be("COMPLETED"); + item["expiration"].N.Should().Be(expiry.ToString()); + } + + [Fact] + public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlreadyExistsException() + { + // Arrange + var key = CreateKey("key"); + + // Insert a fake item with same id + Dictionary item = new(key); + var now = DateTimeOffset.UtcNow; + var expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue {N = expiry.ToString()}); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); + item.Add("data", new AttributeValue("Fake Data")); + await Client.PutItemAsync(new PutItemRequest + { + TableName = TableName, + Item = item + }); + var expiry2 = now.AddSeconds(3600).ToUnixTimeSeconds(); + + // Act + var act = () => DynamoDbPersistenceStore.PutRecord( + new DataRecord("key", + DataRecord.DataRecordStatus.INPROGRESS, + expiry2, + null, + null + ), now); + + // Assert + await act.Should().ThrowAsync(); + + // item was not updated, retrieve the initial one + var itemInDb = (await Client.GetItemAsync(new GetItemRequest + { + TableName = TableName, + Key = key + })).Item; + itemInDb.Should().NotBeNull(); + itemInDb["status"].S.Should().Be("COMPLETED"); + itemInDb["expiration"].N.Should().Be(expiry.ToString()); + itemInDb["data"].S.Should().Be("Fake Data"); + } + + //getRecord + [Fact] + public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecord() + { + // Arrange + //await InitializeAsync(); + + // Insert a fake item with same id + Dictionary item = new() + { + {"id", new AttributeValue("key")} //key + }; + var now = DateTimeOffset.UtcNow; + var expiry = now.AddSeconds(30).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue + { + N = expiry.ToString() + }); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); + item.Add("data", new AttributeValue("Fake Data")); + var _ = await Client.PutItemAsync(new PutItemRequest + { + TableName = TableName, + Item = item + }); + + // Act + var record = await DynamoDbPersistenceStore.GetRecord("key"); + + // Assert + record.IdempotencyKey.Should().Be("key"); + record.Status.Should().Be(DataRecord.DataRecordStatus.COMPLETED); + record.ResponseData.Should().Be("Fake Data"); + record.ExpiryTimestamp.Should().Be(expiry); + } + + [Fact] + public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() + { + // Act + Func act = () => DynamoDbPersistenceStore.GetRecord("key"); + + // Assert + await act.Should().ThrowAsync(); + } + //updateRecord + + [Fact] + public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() + { + // Arrange: Insert a fake item with same id + var key = CreateKey("key"); + Dictionary item = new(key); + var now = DateTimeOffset.UtcNow; + var expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue + { + N = expiry.ToString() + }); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); + await Client.PutItemAsync(new PutItemRequest + { + TableName = TableName, + Item = item + }); + // enable payload validation + DynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().WithPayloadValidationJmesPath("path").Build(), + null); + + // Act + expiry = now.AddSeconds(3600).ToUnixTimeMilliseconds(); + var record = new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, "Fake result", "hash"); + await DynamoDbPersistenceStore.UpdateRecord(record); + + // Assert + var itemInDb = (await Client.GetItemAsync(new GetItemRequest + { + TableName = TableName, + Key = key + })).Item; + + itemInDb["status"].S.Should().Be("COMPLETED"); + itemInDb["expiration"].N.Should().Be(expiry.ToString()); + itemInDb["data"].S.Should().Be("Fake result"); + itemInDb["validation"].S.Should().Be("hash"); + } + + //deleteRecord + [Fact] + public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() + { + // Arrange: Insert a fake item with same id + var key = CreateKey("key"); + Dictionary item = new(key); + var now = DateTimeOffset.UtcNow; + var expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); + item.Add("expiration", new AttributeValue {N=expiry.ToString()}); + item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); + await Client.PutItemAsync(new PutItemRequest + { + TableName = TableName, + Item = item + }); + var scanResponse = await Client.ScanAsync(new ScanRequest + { + TableName = TableName + }); + scanResponse.Items.Count.Should().Be(1); + + // Act + await DynamoDbPersistenceStore.DeleteRecord("key"); + + // Assert + scanResponse = await Client.ScanAsync(new ScanRequest + { + TableName = TableName + }); + scanResponse.Items.Count.Should().Be(0); + } + + [Fact] + public async Task EndToEndWithCustomAttrNamesAndSortKey() + { + const string tableNameCustom = "idempotency_table_custom"; + try + { + var createTableRequest = new CreateTableRequest + { + TableName = tableNameCustom, + KeySchema = new List + { + new("key", KeyType.HASH), + new("sortkey", KeyType.RANGE) + }, + AttributeDefinitions = new List + { + new("key", ScalarAttributeType.S), + new("sortkey", ScalarAttributeType.S) + }, + BillingMode = BillingMode.PAY_PER_REQUEST + }; + await Client.CreateTableAsync(createTableRequest); + var persistenceStore = new DynamoDBPersistenceStoreBuilder() + .WithTableName(tableNameCustom) + .WithDynamoDBClient(Client) + .WithDataAttr("result") + .WithExpiryAttr("expiry") + .WithKeyAttr("key") + .WithSortKeyAttr("sortkey") + .WithStaticPkValue("pk") + .WithStatusAttr("state") + .WithValidationAttr("valid") + .Build(); + persistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); + + var now = DateTimeOffset.UtcNow; + var record = new DataRecord( + "mykey", + DataRecord.DataRecordStatus.INPROGRESS, + now.AddSeconds(400).ToUnixTimeMilliseconds(), + null, + null + ); + // PUT + await persistenceStore.PutRecord(record, now); + + Dictionary customKey = new() + { + { "key", new AttributeValue("pk") }, + { "sortkey", new AttributeValue("mykey") } + }; + + var itemInDb = (await Client.GetItemAsync(new GetItemRequest + { + TableName = tableNameCustom, + Key = customKey + })).Item; + + // GET + var recordInDb = await persistenceStore.GetRecord("mykey"); + + itemInDb.Should().NotBeNull(); + itemInDb["key"].S.Should().Be("pk"); + itemInDb["sortkey"].S.Should().Be(recordInDb.IdempotencyKey); + itemInDb["state"].S.Should().Be(recordInDb.Status.ToString()); + itemInDb["expiry"].N.Should().Be(recordInDb.ExpiryTimestamp.ToString()); + + // UPDATE + var updatedRecord = new DataRecord( + "mykey", + DataRecord.DataRecordStatus.COMPLETED, + now.AddSeconds(500).ToUnixTimeMilliseconds(), + "response", + null + ); + await persistenceStore.UpdateRecord(updatedRecord); + recordInDb = await persistenceStore.GetRecord("mykey"); + recordInDb.Should().Be(updatedRecord); + + // DELETE + await persistenceStore.DeleteRecord("mykey"); + (await Client.ScanAsync(new ScanRequest + { + TableName = tableNameCustom + })).Count.Should().Be(0); + + } + finally + { + try + { + await Client.DeleteTableAsync(new DeleteTableRequest + { + TableName = tableNameCustom + }); + } + catch (Exception) + { + // OK + } + } + } + + [Fact] + public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() + { + try + { + // Arrange + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); + + var store = new DynamoDBPersistenceStoreBuilder().WithTableName(TableName).Build(); + + // Act + Func act = () => store.GetRecord("fake"); + + // Assert + await act.Should().ThrowAsync(); + } + finally + { + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); + } + } + private static Dictionary CreateKey(string keyValue) + { + var key = new Dictionary + { + {"id", new AttributeValue(keyValue)} + }; + return key; + } +} \ No newline at end of file From c2167a1e609acd74820dd5c9a342b4f00e97b840 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Wed, 24 May 2023 10:54:53 +0100 Subject: [PATCH 22/32] forgot to skip tests --- .../IdempotencyTest.cs | 3 +-- .../Persistence/BasePersistenceStoreTests.cs | 4 ++-- .../Persistence/DynamoDBPersistenceStoreTests.cs | 16 ++++++++-------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs index 5cb1c801..f324d09d 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -30,9 +30,8 @@ public class IdempotencyTest { private const string TableName = "idempotency_table"; - //[Fact(Skip = "Integration Tests - Require setup")] + [Fact(Skip = "Integration Tests - Require setup")] [Trait("Category", "Integration")] - [Fact] public async Task EndToEndTest() { var client = new AmazonDynamoDBClient(); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs index 75281f4b..43dbbc38 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/BasePersistenceStoreTests.cs @@ -30,9 +30,9 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; public class BasePersistenceStoreTests { - private class InMemoryPersistenceStore : BasePersistenceStore + class InMemoryPersistenceStore : BasePersistenceStore { - private readonly string _validationHash = null; + private string _validationHash = null; public DataRecord DataRecord; public int Status = -1; public override Task GetRecord(string idempotencyKey) diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index 7b6a4142..01bfbe96 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -31,7 +31,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; public class DynamoDbPersistenceStoreTests : IntegrationTestBase { //putRecord - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB() { // Arrange @@ -57,7 +57,7 @@ await Client.GetItemAsync(new GetItemRequest item["expiration"].N.Should().Be(expiry.ToString()); } - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlreadyExistsException() { // Arrange @@ -102,7 +102,7 @@ await Client.PutItemAsync(new PutItemRequest } //getRecord - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecord() { // Arrange @@ -137,7 +137,7 @@ public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecor record.ExpiryTimestamp.Should().Be(expiry); } - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() { // Act @@ -148,7 +148,7 @@ public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() } //updateRecord - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() { // Arrange: Insert a fake item with same id @@ -189,7 +189,7 @@ await Client.PutItemAsync(new PutItemRequest } //deleteRecord - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() { // Arrange: Insert a fake item with same id @@ -221,7 +221,7 @@ await Client.PutItemAsync(new PutItemRequest scanResponse.Items.Count.Should().Be(0); } - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task EndToEndWithCustomAttrNamesAndSortKey() { const string tableNameCustom = "idempotency_table_custom"; @@ -324,7 +324,7 @@ await Client.DeleteTableAsync(new DeleteTableRequest } } - [Fact] + [Fact(Skip = "Integration Tests - Require setup")] public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() { try From 62ed43321b0f9375b9e29aa2f2b3fbefb07c4708 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Wed, 24 May 2023 13:25:00 +0100 Subject: [PATCH 23/32] adding integration tests. added testcontainers. refactor for single container creation TestFixture. --- ...Lambda.Powertools.Idempotency.Tests.csproj | 1 + .../DynamoDBFixture.cs} | 46 +++++---- .../DynamoDBPersistenceStoreTests.cs | 94 +++++++++++-------- 3 files changed, 83 insertions(+), 58 deletions(-) rename libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/{IntegrationTestBase.cs => Persistence/DynamoDBFixture.cs} (61%) diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj index d381afe4..3f8084de 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/AWS.Lambda.Powertools.Idempotency.Tests.csproj @@ -14,6 +14,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs similarity index 61% rename from libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs rename to libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs index 50e3cd27..6c7c4a2f 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IntegrationTestBase.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs @@ -15,31 +15,44 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; using AWS.Lambda.Powertools.Idempotency.Persistence; -using Xunit; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; -namespace AWS.Lambda.Powertools.Idempotency.Tests; +namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; -public class IntegrationTestBase : IAsyncLifetime +// ReSharper disable once ClassNeverInstantiated.Global +public class DynamoDbFixture : IDisposable { - protected const string TableName = "idempotency_table"; - protected AmazonDynamoDBClient Client; - private protected DynamoDBPersistenceStore DynamoDbPersistenceStore; + private readonly IContainer _container; + public AmazonDynamoDBClient Client { get; set; } + public DynamoDBPersistenceStore DynamoDbPersistenceStore { get; set; } + public string TableName { get; set; } = "idempotency_table"; - public async Task InitializeAsync() + public DynamoDbFixture() { - // initialize TestContainers or Ductus.FluentDocker or have DynamoDb local docker running + Environment.SetEnvironmentVariable("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE","/var/run/docker.sock"); + + _container = new ContainerBuilder() + .WithName(Guid.NewGuid().ToString("D")) + .WithImage("amazon/dynamodb-local:latest") + .WithPortBinding(8000, true) + .WithDockerEndpoint(Environment.GetEnvironmentVariable("DOCKER_HOST") ?? "unix:///var/run/docker.sock") + .Build(); + + + _container.StartAsync().Wait(); var credentials = new BasicAWSCredentials("FAKE", "FAKE"); var amazonDynamoDbConfig = new AmazonDynamoDBConfig { - ServiceURL = new UriBuilder("http", "localhost", 8000).Uri.ToString(), + ServiceURL = new UriBuilder("http", _container.Hostname, _container.GetMappedPublicPort(8000)).Uri.ToString(), AuthenticationRegion = "us-east-1" }; + Client = new AmazonDynamoDBClient(credentials, amazonDynamoDbConfig); var createTableRequest = new CreateTableRequest @@ -61,8 +74,8 @@ public async Task InitializeAsync() }; try { - await Client.CreateTableAsync(createTableRequest); - var response = await Client.DescribeTableAsync(TableName); + Client.CreateTableAsync(createTableRequest).GetAwaiter().GetResult(); + var response = Client.DescribeTableAsync(TableName).GetAwaiter().GetResult(); if (response == null) { throw new NullReferenceException("Table was not created within the expected time"); @@ -79,12 +92,9 @@ public async Task InitializeAsync() .Build(); DynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); } - - public Task DisposeAsync() + + public void Dispose() { - // Make sure delete item after each test - DynamoDbPersistenceStore.DeleteRecord("key").ConfigureAwait(false); - //_dynamoContainer.DisposeAsync().ConfigureAwait(false); - return Task.CompletedTask; + _container.DisposeAsync().AsTask(); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index 01bfbe96..84bfe6fc 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -28,10 +28,21 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Persistence; [Collection("Sequential")] [Trait("Category", "Integration")] -public class DynamoDbPersistenceStoreTests : IntegrationTestBase +public class DynamoDbPersistenceStoreTests : IClassFixture { + private readonly DynamoDBPersistenceStore _dynamoDbPersistenceStore; + private readonly AmazonDynamoDBClient _client; + private readonly string _tableName; + + public DynamoDbPersistenceStoreTests(DynamoDbFixture fixture) + { + _dynamoDbPersistenceStore = fixture.DynamoDbPersistenceStore; + _client = fixture.Client; + _tableName = fixture.TableName; + } + //putRecord - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB() { // Arrange @@ -40,14 +51,14 @@ public async Task PutRecord_WhenRecordDoesNotExist_ShouldCreateRecordInDynamoDB( var key = CreateKey("key"); // Act - await DynamoDbPersistenceStore + await _dynamoDbPersistenceStore .PutRecord(new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, null, null), now); // Assert var getItemResponse = - await Client.GetItemAsync(new GetItemRequest + await _client.GetItemAsync(new GetItemRequest { - TableName = TableName, + TableName = _tableName, Key = key }); @@ -57,7 +68,7 @@ await Client.GetItemAsync(new GetItemRequest item["expiration"].N.Should().Be(expiry.ToString()); } - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlreadyExistsException() { // Arrange @@ -70,15 +81,15 @@ public async Task PutRecord_WhenRecordAlreadyExist_ShouldThrowIdempotencyItemAlr item.Add("expiration", new AttributeValue {N = expiry.ToString()}); item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); item.Add("data", new AttributeValue("Fake Data")); - await Client.PutItemAsync(new PutItemRequest + await _client.PutItemAsync(new PutItemRequest { - TableName = TableName, + TableName = _tableName, Item = item }); var expiry2 = now.AddSeconds(3600).ToUnixTimeSeconds(); // Act - var act = () => DynamoDbPersistenceStore.PutRecord( + var act = () => _dynamoDbPersistenceStore.PutRecord( new DataRecord("key", DataRecord.DataRecordStatus.INPROGRESS, expiry2, @@ -90,9 +101,9 @@ await Client.PutItemAsync(new PutItemRequest await act.Should().ThrowAsync(); // item was not updated, retrieve the initial one - var itemInDb = (await Client.GetItemAsync(new GetItemRequest + var itemInDb = (await _client.GetItemAsync(new GetItemRequest { - TableName = TableName, + TableName = _tableName, Key = key })).Item; itemInDb.Should().NotBeNull(); @@ -102,7 +113,7 @@ await Client.PutItemAsync(new PutItemRequest } //getRecord - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecord() { // Arrange @@ -121,14 +132,14 @@ public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecor }); item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.COMPLETED.ToString())); item.Add("data", new AttributeValue("Fake Data")); - var _ = await Client.PutItemAsync(new PutItemRequest + var _ = await _client.PutItemAsync(new PutItemRequest { - TableName = TableName, + TableName = _tableName, Item = item }); // Act - var record = await DynamoDbPersistenceStore.GetRecord("key"); + var record = await _dynamoDbPersistenceStore.GetRecord("key"); // Assert record.IdempotencyKey.Should().Be("key"); @@ -137,18 +148,21 @@ public async Task GetRecord_WhenRecordExistsInDynamoDb_ShouldReturnExistingRecor record.ExpiryTimestamp.Should().Be(expiry); } - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task GetRecord_WhenRecordIsAbsent_ShouldThrowException() { + //Arrange + await _dynamoDbPersistenceStore.DeleteRecord("key"); + // Act - Func act = () => DynamoDbPersistenceStore.GetRecord("key"); + Func act = () => _dynamoDbPersistenceStore.GetRecord("key"); // Assert await act.Should().ThrowAsync(); } //updateRecord - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() { // Arrange: Insert a fake item with same id @@ -161,24 +175,24 @@ public async Task UpdateRecord_WhenRecordExistsInDynamoDb_ShouldUpdateRecord() N = expiry.ToString() }); item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); - await Client.PutItemAsync(new PutItemRequest + await _client.PutItemAsync(new PutItemRequest { - TableName = TableName, + TableName = _tableName, Item = item }); // enable payload validation - DynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().WithPayloadValidationJmesPath("path").Build(), + _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().WithPayloadValidationJmesPath("path").Build(), null); // Act expiry = now.AddSeconds(3600).ToUnixTimeMilliseconds(); var record = new DataRecord("key", DataRecord.DataRecordStatus.COMPLETED, expiry, "Fake result", "hash"); - await DynamoDbPersistenceStore.UpdateRecord(record); + await _dynamoDbPersistenceStore.UpdateRecord(record); // Assert - var itemInDb = (await Client.GetItemAsync(new GetItemRequest + var itemInDb = (await _client.GetItemAsync(new GetItemRequest { - TableName = TableName, + TableName = _tableName, Key = key })).Item; @@ -189,7 +203,7 @@ await Client.PutItemAsync(new PutItemRequest } //deleteRecord - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() { // Arrange: Insert a fake item with same id @@ -199,29 +213,29 @@ public async Task DeleteRecord_WhenRecordExistsInDynamoDb_ShouldDeleteRecord() var expiry = now.AddSeconds(360).ToUnixTimeMilliseconds(); item.Add("expiration", new AttributeValue {N=expiry.ToString()}); item.Add("status", new AttributeValue(DataRecord.DataRecordStatus.INPROGRESS.ToString())); - await Client.PutItemAsync(new PutItemRequest + await _client.PutItemAsync(new PutItemRequest { - TableName = TableName, + TableName = _tableName, Item = item }); - var scanResponse = await Client.ScanAsync(new ScanRequest + var scanResponse = await _client.ScanAsync(new ScanRequest { - TableName = TableName + TableName = _tableName }); scanResponse.Items.Count.Should().Be(1); // Act - await DynamoDbPersistenceStore.DeleteRecord("key"); + await _dynamoDbPersistenceStore.DeleteRecord("key"); // Assert - scanResponse = await Client.ScanAsync(new ScanRequest + scanResponse = await _client.ScanAsync(new ScanRequest { - TableName = TableName + TableName = _tableName }); scanResponse.Items.Count.Should().Be(0); } - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task EndToEndWithCustomAttrNamesAndSortKey() { const string tableNameCustom = "idempotency_table_custom"; @@ -242,10 +256,10 @@ public async Task EndToEndWithCustomAttrNamesAndSortKey() }, BillingMode = BillingMode.PAY_PER_REQUEST }; - await Client.CreateTableAsync(createTableRequest); + await _client.CreateTableAsync(createTableRequest); var persistenceStore = new DynamoDBPersistenceStoreBuilder() .WithTableName(tableNameCustom) - .WithDynamoDBClient(Client) + .WithDynamoDBClient(_client) .WithDataAttr("result") .WithExpiryAttr("expiry") .WithKeyAttr("key") @@ -273,7 +287,7 @@ public async Task EndToEndWithCustomAttrNamesAndSortKey() { "sortkey", new AttributeValue("mykey") } }; - var itemInDb = (await Client.GetItemAsync(new GetItemRequest + var itemInDb = (await _client.GetItemAsync(new GetItemRequest { TableName = tableNameCustom, Key = customKey @@ -302,7 +316,7 @@ public async Task EndToEndWithCustomAttrNamesAndSortKey() // DELETE await persistenceStore.DeleteRecord("mykey"); - (await Client.ScanAsync(new ScanRequest + (await _client.ScanAsync(new ScanRequest { TableName = tableNameCustom })).Count.Should().Be(0); @@ -312,7 +326,7 @@ public async Task EndToEndWithCustomAttrNamesAndSortKey() { try { - await Client.DeleteTableAsync(new DeleteTableRequest + await _client.DeleteTableAsync(new DeleteTableRequest { TableName = tableNameCustom }); @@ -324,7 +338,7 @@ await Client.DeleteTableAsync(new DeleteTableRequest } } - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() { try @@ -332,7 +346,7 @@ public async Task GetRecord_WhenIdempotencyDisabled_ShouldNotCreateClients() // Arrange Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); - var store = new DynamoDBPersistenceStoreBuilder().WithTableName(TableName).Build(); + var store = new DynamoDBPersistenceStoreBuilder().WithTableName(_tableName).Build(); // Act Func act = () => store.GetRecord("fake"); From 2424868e04d69ad13232fb858861845de1e03ec9 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Thu, 25 May 2023 11:46:52 +0100 Subject: [PATCH 24/32] Refactor integration tests. Now all run in TestContainer --- .../IdempotencyTest.cs | 22 ++++++++++++------- .../Persistence/DynamoDBFixture.cs | 7 ------ .../DynamoDBPersistenceStoreTests.cs | 8 +++++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs index f324d09d..f3c4d4ed 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -21,22 +21,28 @@ using Amazon.Lambda.APIGatewayEvents; using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Idempotency.Tests.Handlers; +using AWS.Lambda.Powertools.Idempotency.Tests.Persistence; using FluentAssertions; using Xunit; namespace AWS.Lambda.Powertools.Idempotency.Tests; -public class IdempotencyTest +public class IdempotencyTest : IClassFixture { - private const string TableName = "idempotency_table"; + private readonly AmazonDynamoDBClient _client; + private readonly string _tableName; + + public IdempotencyTest(DynamoDbFixture fixture) + { + _client = fixture.Client; + _tableName = fixture.TableName; + } - [Fact(Skip = "Integration Tests - Require setup")] + [Fact] [Trait("Category", "Integration")] public async Task EndToEndTest() { - var client = new AmazonDynamoDBClient(); - - var function = new IdempotencyFunction(client); + var function = new IdempotencyFunction(_client); var options = new JsonSerializerOptions { @@ -58,9 +64,9 @@ public async Task EndToEndTest() JsonSerializer.Serialize(response).Should().Be(JsonSerializer.Serialize(response)); response2.Body.Should().Contain("hello world"); - var scanResponse = await client.ScanAsync(new ScanRequest + var scanResponse = await _client.ScanAsync(new ScanRequest { - TableName = TableName + TableName = _tableName }); scanResponse.Count.Should().Be(1); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs index 6c7c4a2f..ca37c459 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs @@ -29,7 +29,6 @@ public class DynamoDbFixture : IDisposable { private readonly IContainer _container; public AmazonDynamoDBClient Client { get; set; } - public DynamoDBPersistenceStore DynamoDbPersistenceStore { get; set; } public string TableName { get; set; } = "idempotency_table"; public DynamoDbFixture() @@ -85,12 +84,6 @@ public DynamoDbFixture() { Console.WriteLine(e.Message); } - - DynamoDbPersistenceStore = new DynamoDBPersistenceStoreBuilder() - .WithTableName(TableName) - .WithDynamoDBClient(Client) - .Build(); - DynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); } public void Dispose() diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs index 84bfe6fc..a6f61531 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBPersistenceStoreTests.cs @@ -33,12 +33,16 @@ public class DynamoDbPersistenceStoreTests : IClassFixture private readonly DynamoDBPersistenceStore _dynamoDbPersistenceStore; private readonly AmazonDynamoDBClient _client; private readonly string _tableName; - + public DynamoDbPersistenceStoreTests(DynamoDbFixture fixture) { - _dynamoDbPersistenceStore = fixture.DynamoDbPersistenceStore; _client = fixture.Client; _tableName = fixture.TableName; + _dynamoDbPersistenceStore = new DynamoDBPersistenceStoreBuilder() + .WithTableName(_tableName) + .WithDynamoDBClient(_client) + .Build(); + _dynamoDbPersistenceStore.Configure(new IdempotencyOptionsBuilder().Build(),functionName: null); } //putRecord From b6b3c7f3ce7dae3632f9ef73b015d118a6ae8c4d Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Thu, 25 May 2023 15:45:59 +0100 Subject: [PATCH 25/32] Refactor tests. Remove direct environment check for Idempotency Disabled. --- .../IdempotentAttribute.cs | 3 +- .../Output/ConsoleLog.cs | 66 -------------- .../Persistence/DynamoDBPersistenceStore.cs | 4 +- .../Internal/IdempotencyTests.cs | 30 +------ .../Internal/IdempotentAspectTests.cs | 85 +++++++++++++------ 5 files changed, 60 insertions(+), 128 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 882fa87c..2f4e5fc8 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -83,8 +83,7 @@ protected internal sealed override T WrapSync(Func target, objec protected internal sealed override async Task WrapAsync( Func> target, object[] args, AspectEventArgs eventArgs) { - var idempotencyDisabledEnv = Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv); - if (idempotencyDisabledEnv is "true") + if (PowertoolsConfigurations.Instance.IdempotencyDisabled) { return await base.WrapAsync(target, args, eventArgs); } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs deleted file mode 100644 index 3c63e0aa..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ConsoleLog.cs +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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; - -namespace AWS.Lambda.Powertools.Idempotency.Output; - -/// -/// A log that writes to the console in a colorful way. -/// -public class ConsoleLog : ILog -{ - /// - /// Writes an informational message to the log. - /// - /// The format. - /// The args. - public void WriteInformation(string format, params object[] args) => Write(ConsoleColor.White, format, args); - - /// - /// Writes an error message to the log. - /// - /// The format. - /// The args. - public void WriteError(string format, params object[] args) => Write(ConsoleColor.Red, format, args); - - /// - /// Writes a warning message to the log. - /// - /// The format. - /// The args. - public void WriteWarning(string format, params object[] args) => Write(ConsoleColor.Yellow, format, args); - - /// - /// Writes a debug message to the log. - /// - /// The format. - /// The args. - public void WriteDebug(string format, params object[] args) => Write(ConsoleColor.Cyan, format, args); - - private static void Write(ConsoleColor color, string format, object[] args) - { - var oldColor = Console.ForegroundColor; - Console.ForegroundColor = color; - try - { - Console.WriteLine(format, args); - } - finally - { - Console.ForegroundColor = oldColor; - } - } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index 3b4c8b14..b03e9f04 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -65,9 +65,7 @@ internal DynamoDBPersistenceStore(string tableName, } else { - var isIdempotencyDisabled = bool.TryParse(Environment.GetEnvironmentVariable(Constants.IdempotencyDisabledEnv), out var result) && result; - - if (isIdempotencyDisabled) + if (PowertoolsConfigurations.Instance.IdempotencyDisabled) { // we do not want to create a DynamoDbClient if idempotency is disabled // null is ok as idempotency won't be called diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs index daeb2166..2a66c8b1 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs @@ -6,33 +6,5 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; public class IdempotencyTests { - [Fact] - public void Idempotency_Set_Execution_Environment_Context() - { - // Arrange - var assemblyName = "AWS.Lambda.Powertools.Idempotency"; - var assemblyVersion = "1.0.0"; - - var env = new Mock(); - env.Setup(x => x.GetAssemblyName(It.IsAny())).Returns(assemblyName); - env.Setup(x => x.GetAssemblyVersion(It.IsAny())).Returns(assemblyVersion); - - var conf = new PowertoolsConfigurations(new SystemWrapper(env.Object)); - - // Act - var xRayRecorder = new Idempotency(conf); - - // Assert - env.Verify(v => - v.SetEnvironmentVariable( - "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Idempotency/{assemblyVersion}" - ), Times.Once); - - env.Verify(v => - v.GetEnvironmentVariable( - "AWS_EXECUTION_ENV" - ), Times.Once); - - Assert.NotNull(xRayRecorder); - } + } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index f7a9033e..037a083a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -31,7 +31,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; [Collection("Sequential")] -public class IdempotentAspectTests +public class IdempotentAspectTests : IDisposable { [Fact] public async Task Handle_WhenFirstCall_ShouldPutInStore() @@ -157,33 +157,62 @@ public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionE [Fact] public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() { - try - { - // Arrange - var store = new Mock(); - - Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); - - Idempotency.Configure(builder => - builder - .WithPersistenceStore(store.Object) - .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) - ); - - var function = new IdempotencyEnabledFunction(); - var product = new Product(42, "fake product", 12); - - // Act - var basket = await function.Handle(product, new TestLambdaContext()); + + // Arrange + var store = new Mock(); + + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "true"); + + Idempotency.Configure(builder => + builder + .WithPersistenceStore(store.Object) + .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) + ); + + var function = new IdempotencyEnabledFunction(); + var product = new Product(42, "fake product", 12); + + // Act + var basket = await function.Handle(product, new TestLambdaContext()); + + // Assert + store.Invocations.Count.Should().Be(0); + basket.Products.Count.Should().Be(1); + function.HandlerExecuted.Should().BeTrue(); + } - // Assert - store.Invocations.Count.Should().Be(0); - basket.Products.Count.Should().Be(1); - function.HandlerExecuted.Should().BeTrue(); - } - finally - { - Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); - } + [Fact] + public void Idempotency_Set_Execution_Environment_Context() + { + // Arrange + var assemblyName = "AWS.Lambda.Powertools.Idempotency"; + var assemblyVersion = "1.0.0"; + + var env = new Mock(); + env.Setup(x => x.GetAssemblyName(It.IsAny())).Returns(assemblyName); + env.Setup(x => x.GetAssemblyVersion(It.IsAny())).Returns(assemblyVersion); + + var conf = new PowertoolsConfigurations(new SystemWrapper(env.Object)); + + // Act + var xRayRecorder = new Idempotency(conf); + + // Assert + env.Verify(v => + v.SetEnvironmentVariable( + "AWS_EXECUTION_ENV", $"{Constants.FeatureContextIdentifier}/Idempotency/{assemblyVersion}" + ), Times.Once); + + env.Verify(v => + v.GetEnvironmentVariable( + "AWS_EXECUTION_ENV" + ), Times.Once); + + Assert.NotNull(xRayRecorder); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(Constants.IdempotencyDisabledEnv, "false"); } } \ No newline at end of file From 983234f3526f5858de85d1fb29f9f3bb573bbaeb Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Thu, 25 May 2023 15:47:20 +0100 Subject: [PATCH 26/32] delete file --- .../Internal/IdempotencyTests.cs | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs deleted file mode 100644 index 2a66c8b1..00000000 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotencyTests.cs +++ /dev/null @@ -1,10 +0,0 @@ -using AWS.Lambda.Powertools.Common; -using Moq; -using Xunit; - -namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; - -public class IdempotencyTests -{ - -} \ No newline at end of file From 45c746a5d0b6a901a93d998b7cd0fc4cf77a3d67 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Fri, 26 May 2023 10:30:35 +0100 Subject: [PATCH 27/32] Refactor. Enabled Idempotency on Sync handlers. Refactor unit tests with Theory for both async and sync handlers. --- .../IdempotentAttribute.cs | 26 ++++++++-- .../Internal/IdempotencyAspectHandler.cs | 9 ++-- .../Handlers/IdempotencyEnabledFunction.cs | 40 ++++++++++++-- .../Handlers/IdempotencyWithErrorFunction.cs | 24 ++++++++- .../Internal/IdempotentAspectTests.cs | 52 +++++++++++-------- 5 files changed, 116 insertions(+), 35 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 2f4e5fc8..2246c725 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -69,9 +69,27 @@ public class IdempotentAttribute : UniversalWrapperAttribute /// T. protected internal sealed override T WrapSync(Func target, object[] args, AspectEventArgs eventArgs) { - throw new IdempotencyConfigurationException("Idempotent attribute can be used on async methods only"); + if (PowertoolsConfigurations.Instance.IdempotencyDisabled) + { + return base.WrapSync(target, args, eventArgs); + } + var payload = JsonDocument.Parse(JsonSerializer.Serialize(args[0])); + if (payload == null) + { + throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); + } + + Task ResultDelegate() => Task.FromResult(target(args)); + + var idempotencyHandler = new IdempotencyAspectHandler(ResultDelegate, eventArgs.Method.Name, payload); + if (idempotencyHandler == null) + { + throw new Exception("Failed to create an instance of IdempotencyAspectHandler"); + } + var result = idempotencyHandler.Handle().GetAwaiter().GetResult(); + return result; } - + /// /// Wrap as an asynchronous operation. /// @@ -93,7 +111,9 @@ protected internal sealed override async Task WrapAsync( throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); } - var idempotencyHandler = new IdempotencyAspectHandler(target, args, eventArgs.Method.Name, payload); + Task ResultDelegate() => target(args); + + var idempotencyHandler = new IdempotencyAspectHandler(ResultDelegate, eventArgs.Method.Name, payload); if (idempotencyHandler == null) { throw new Exception("Failed to create an instance of IdempotencyAspectHandler"); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs index 7901c80d..8b25bc23 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs @@ -26,20 +26,17 @@ internal class IdempotencyAspectHandler { private const int MaxRetries = 2; - private readonly Func _target; - private readonly object[] _args; + private readonly Func> _target; private readonly JsonDocument _data; private readonly BasePersistenceStore _persistenceStore; private readonly ILog _log; public IdempotencyAspectHandler( - Func target, - object[] args, + Func> target, string functionName, JsonDocument payload) { _target = target; - _args = args; _data = payload; _persistenceStore = Idempotency.Instance.PersistenceStore; _persistenceStore.Configure(Idempotency.Instance.IdempotencyOptions, functionName); @@ -182,7 +179,7 @@ private async Task GetFunctionResponse() T response; try { - response = await (Task)_target(_args); + response = await _target(); } catch(Exception handlerException) { diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs index d18be44d..5ba3e816 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyEnabledFunction.cs @@ -19,18 +19,50 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; -public class IdempotencyEnabledFunction + +public interface IIdempotencyEnabledFunction { - public bool HandlerExecuted; + public bool HandlerExecuted { get; set; } + Task HandleTest(Product input, ILambdaContext context); +} +public class IdempotencyEnabledFunction : IIdempotencyEnabledFunction +{ [Idempotent] - public Task Handle(Product input, ILambdaContext context) + public async Task Handle(Product input, ILambdaContext context) { HandlerExecuted = true; var basket = new Basket(); basket.Add(input); var result = Task.FromResult(basket); - return result; + return await result; + } + + public bool HandlerExecuted { get; set; } + + public Task HandleTest(Product input, ILambdaContext context) + { + return Handle(input, context); + } +} + +public class IdempotencyEnabledSyncFunction : IIdempotencyEnabledFunction +{ + [Idempotent] + public Basket Handle(Product input, ILambdaContext context) + { + HandlerExecuted = true; + var basket = new Basket(); + basket.Add(input); + + return basket; + } + + public bool HandlerExecuted { get; set; } + + public Task HandleTest(Product input, ILambdaContext context) + { + return Task.FromResult(Handle(input, context)); } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs index 645f3fc2..8dbcd331 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyWithErrorFunction.cs @@ -20,9 +20,31 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; -public class IdempotencyWithErrorFunction +public interface IIdempotencyWithErrorFunction +{ + Task HandleTest(Product input, ILambdaContext context); +} + +public class IdempotencyWithErrorFunction : IIdempotencyWithErrorFunction { [Idempotent] public Task Handle(Product input, ILambdaContext context) => throw new IndexOutOfRangeException("Fake exception"); + + public Task HandleTest(Product input, ILambdaContext context) + { + return Handle(input, context); + } +} + +public class IdempotencyWithErrorSyncFunction : IIdempotencyWithErrorFunction +{ + [Idempotent] + public Basket Handle(Product input, ILambdaContext context) + => throw new IndexOutOfRangeException("Fake exception"); + + public Task HandleTest(Product input, ILambdaContext context) + { + return Task.FromResult(Handle(input, context)); + } } \ No newline at end of file diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs index 037a083a..b850d82a 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/IdempotentAspectTests.cs @@ -33,8 +33,10 @@ namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; [Collection("Sequential")] public class IdempotentAspectTests : IDisposable { - [Fact] - public async Task Handle_WhenFirstCall_ShouldPutInStore() + [Theory] + [InlineData(typeof(IdempotencyEnabledFunction))] + [InlineData(typeof(IdempotencyEnabledSyncFunction))] + public async Task Handle_WhenFirstCall_ShouldPutInStore(Type type) { //Arrange var store = new Mock(); @@ -44,11 +46,11 @@ public async Task Handle_WhenFirstCall_ShouldPutInStore() .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - var function = new IdempotencyEnabledFunction(); + var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction; var product = new Product(42, "fake product", 12); //Act - var basket = await function.Handle(product, new TestLambdaContext()); + var basket = await function!.HandleTest(product, new TestLambdaContext()); //Assert basket.Products.Count.Should().Be(1); @@ -61,8 +63,10 @@ public async Task Handle_WhenFirstCall_ShouldPutInStore() .Verify(x=>x.SaveSuccess(It.IsAny(), It.Is(y => y.Equals(basket)), It.IsAny())); } - [Fact] - public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() + [Theory] + [InlineData(typeof(IdempotencyEnabledFunction))] + [InlineData(typeof(IdempotencyEnabledSyncFunction))] + public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore(Type type) { //Arrange var store = new Mock(); @@ -87,18 +91,20 @@ public async Task Handle_WhenSecondCall_AndNotExpired_ShouldGetFromStore() store.Setup(x=>x.GetRecord(It.IsAny(), It.IsAny())) .ReturnsAsync(record); - var function = new IdempotencyEnabledFunction(); + var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction; // Act - var resultBasket = await function.Handle(product, new TestLambdaContext()); + var resultBasket = await function!.HandleTest(product, new TestLambdaContext()); // Assert resultBasket.Should().Be(basket); function.HandlerExecuted.Should().BeFalse(); } - [Fact] - public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempotencyAlreadyInProgressException() + [Theory] + [InlineData(typeof(IdempotencyEnabledFunction))] + [InlineData(typeof(IdempotencyEnabledSyncFunction))] + public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempotencyAlreadyInProgressException(Type type) { // Arrange var store = new Mock(); @@ -123,15 +129,17 @@ public async Task Handle_WhenSecondCall_AndStatusInProgress_ShouldThrowIdempoten .ReturnsAsync(record); // Act - var function = new IdempotencyEnabledFunction(); - Func act = async () => await function.Handle(product, new TestLambdaContext()); + var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction; + Func act = async () => await function!.HandleTest(product, new TestLambdaContext()); // Assert await act.Should().ThrowAsync(); } - [Fact] - public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionException() + [Theory] + [InlineData(typeof(IdempotencyWithErrorFunction))] + [InlineData(typeof(IdempotencyWithErrorSyncFunction))] + public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionException(Type type) { // Arrange var store = new Mock(); @@ -142,11 +150,11 @@ public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionE .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - var function = new IdempotencyWithErrorFunction(); + var function = Activator.CreateInstance(type) as IIdempotencyWithErrorFunction; var product = new Product(42, "fake product", 12); // Act - Func act = async () => await function.Handle(product, new TestLambdaContext()); + Func act = async () => await function!.HandleTest(product, new TestLambdaContext()); // Assert await act.Should().ThrowAsync(); @@ -154,8 +162,10 @@ public async Task Handle_WhenThrowException_ShouldDeleteRecord_AndThrowFunctionE x => x.DeleteRecord(It.IsAny(), It.IsAny())); } - [Fact] - public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() + [Theory] + [InlineData(typeof(IdempotencyEnabledFunction))] + [InlineData(typeof(IdempotencyEnabledSyncFunction))] + public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction(Type type) { // Arrange @@ -169,18 +179,18 @@ public async Task Handle_WhenIdempotencyDisabled_ShouldJustRunTheFunction() .WithOptions(optionsBuilder => optionsBuilder.WithEventKeyJmesPath("Id")) ); - var function = new IdempotencyEnabledFunction(); + var function = Activator.CreateInstance(type) as IIdempotencyEnabledFunction; var product = new Product(42, "fake product", 12); // Act - var basket = await function.Handle(product, new TestLambdaContext()); + var basket = await function!.HandleTest(product, new TestLambdaContext()); // Assert store.Invocations.Count.Should().Be(0); basket.Products.Count.Should().Be(1); function.HandlerExecuted.Should().BeTrue(); } - + [Fact] public void Idempotency_Set_Execution_Environment_Context() { From be9bcdcd7a21b0b75bac0a3811a1fbb3ef6d295b Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Mon, 29 May 2023 12:30:28 +0100 Subject: [PATCH 28/32] Remove LogTo method, ILog and Debug logging --- .../AWS.Lambda.Powertools.Idempotency.csproj | 4 ++ .../IdempotencyOptions.cs | 11 +--- .../IdempotencyOptionsBuilder.cs | 16 +----- .../Internal/IdempotencyAspectHandler.cs | 6 +-- .../Output/ILog.cs | 50 ------------------- .../Output/NullLog.cs | 40 --------------- .../Persistence/BasePersistenceStore.cs | 14 +----- .../Persistence/DynamoDBPersistenceStore.cs | 5 -- 8 files changed, 9 insertions(+), 137 deletions(-) delete mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs delete mode 100644 libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index 684a6a50..f9445d14 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -43,4 +43,8 @@ + + + + diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs index f74b094f..3cabe8f4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs @@ -13,8 +13,6 @@ * permissions and limitations under the License. */ -using AWS.Lambda.Powertools.Idempotency.Output; - namespace AWS.Lambda.Powertools.Idempotency; /// @@ -59,11 +57,6 @@ public class IdempotencyOptions /// as supported by (eg. SHA1, SHA-256, ...) /// public string HashFunction { get; } - - /// - /// Instance of ILog to record internal details of idempotency - /// - public ILog Log { get; } internal IdempotencyOptions( string eventKeyJmesPath, @@ -72,8 +65,7 @@ internal IdempotencyOptions( bool useLocalCache, int localCacheMaxItems, long expirationInSeconds, - string hashFunction, - ILog log) + string hashFunction) { EventKeyJmesPath = eventKeyJmesPath; PayloadValidationJmesPath = payloadValidationJmesPath; @@ -82,6 +74,5 @@ internal IdempotencyOptions( LocalCacheMaxItems = localCacheMaxItems; ExpirationInSeconds = expirationInSeconds; HashFunction = hashFunction; - Log = log; } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs index 0cda2fb9..daa3ef42 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs @@ -1,5 +1,4 @@ using System; -using AWS.Lambda.Powertools.Idempotency.Output; namespace AWS.Lambda.Powertools.Idempotency; @@ -15,7 +14,6 @@ public class IdempotencyOptionsBuilder private string _payloadValidationJmesPath; private bool _throwOnNoIdempotencyKey; private string _hashFunction = "MD5"; - private ILog _log = new NullLog(); /// /// Initialize and return an instance of IdempotencyConfig. @@ -32,8 +30,7 @@ public IdempotencyOptions Build() => _useLocalCache, _localCacheMaxItems, _expirationInSeconds, - _hashFunction, - _log); + _hashFunction); /// /// A JMESPath expression to extract the idempotency key from the event record. @@ -103,15 +100,4 @@ public IdempotencyOptionsBuilder WithHashFunction(string hashFunction) _hashFunction = hashFunction; return this; } - - /// - /// Logs to a custom logger. - /// - /// The logger. - /// the instance of the builder (to chain operations) - public IdempotencyOptionsBuilder LogTo(ILog log) - { - _log = log; - return this; - } } \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs index 8b25bc23..76be618b 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs @@ -17,7 +17,6 @@ using System.Text.Json; using System.Threading.Tasks; using AWS.Lambda.Powertools.Idempotency.Exceptions; -using AWS.Lambda.Powertools.Idempotency.Output; using AWS.Lambda.Powertools.Idempotency.Persistence; namespace AWS.Lambda.Powertools.Idempotency.Internal; @@ -29,7 +28,6 @@ internal class IdempotencyAspectHandler private readonly Func> _target; private readonly JsonDocument _data; private readonly BasePersistenceStore _persistenceStore; - private readonly ILog _log; public IdempotencyAspectHandler( Func> target, @@ -40,7 +38,6 @@ public IdempotencyAspectHandler( _data = payload; _persistenceStore = Idempotency.Instance.PersistenceStore; _persistenceStore.Configure(Idempotency.Instance.IdempotencyOptions, functionName); - _log = Idempotency.Instance.IdempotencyOptions.Log; } /// @@ -117,7 +114,7 @@ private Task GetIdempotencyRecord() catch (IdempotencyItemNotFoundException e) { // This code path will only be triggered if the record is removed between saveInProgress and getRecord - _log.WriteDebug("An existing idempotency record was deleted before we could fetch it"); + Console.WriteLine("An existing idempotency record was deleted before we could fetch it"); throw new IdempotencyInconsistentStateException("saveInProgress and getRecord return inconsistent results", e); } @@ -159,7 +156,6 @@ private Task HandleForStatus(DataRecord record) default: try { - _log.WriteDebug("Response for key '{0}' retrieved from idempotency store, skipping the function", record.IdempotencyKey); var result = JsonSerializer.Deserialize(record.ResponseData!); if (result is null) { diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs deleted file mode 100644 index 14b35ca1..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/ILog.cs +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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. - */ - -namespace AWS.Lambda.Powertools.Idempotency.Output; - -/// -/// Implemented by objects which record internal details of idempotency -/// -public interface ILog -{ - /// - /// Writes an informational message to the log. - /// - /// The format. - /// The args. - void WriteInformation(string format, params object[] args); - - /// - /// Writes an error message to the log. - /// - /// The format. - /// The args. - void WriteError(string format, params object[] args); - - /// - /// Writes a warning message to the log. - /// - /// The format. - /// The args. - void WriteWarning(string format, params object[] args); - - /// - /// Writes a debug message to the log. - /// - /// The format. - /// The args. - void WriteDebug(string format, params object[] args); -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs deleted file mode 100644 index f58fc688..00000000 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Output/NullLog.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace AWS.Lambda.Powertools.Idempotency.Output; - -/// -/// Does not write any log messages, all method simply return -/// -public class NullLog: ILog -{ - /// - /// Does not write information message, simply return - /// - /// - /// - public void WriteInformation(string format, params object[] args) - { } - - /// - /// Does not write error message, simply return - /// - /// - /// - public void WriteError(string format, params object[] args) - { } - - - /// - /// Does not write warning message, simply return - /// - /// - /// - public void WriteWarning(string format, params object[] args) - { } - - /// - /// Does not write debug message, simply return - /// - /// - /// - public void WriteDebug(string format, params object[] args) - { } -} \ No newline at end of file diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 4a4420e6..a005032c 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -21,7 +21,6 @@ using AWS.Lambda.Powertools.Common; using AWS.Lambda.Powertools.Idempotency.Exceptions; using AWS.Lambda.Powertools.Idempotency.Internal; -using AWS.Lambda.Powertools.Idempotency.Output; using AWS.Lambda.Powertools.Idempotency.Serialization; using DevLab.JmesPath; @@ -41,10 +40,6 @@ public abstract class BasePersistenceStore : IPersistenceStore /// protected bool PayloadValidationEnabled; private LRUCache _cache = null!; - /// - /// Instance of ILog to log the internal details of idempotency - /// - protected ILog Log = null!; /// /// Initialize the base persistence layer from the configuration settings @@ -60,7 +55,6 @@ public void Configure(IdempotencyOptions idempotencyOptions, string functionName _functionName += "." + functionName; } _idempotencyOptions = idempotencyOptions; - Log = _idempotencyOptions.Log; //TODO: optimize to not reconfigure if (!string.IsNullOrWhiteSpace(_idempotencyOptions.PayloadValidationJmesPath)) @@ -100,7 +94,6 @@ public virtual async Task SaveSuccess(JsonDocument data, object result, DateTime responseJson, GetHashedPayload(data) ); - Log.WriteDebug("Function successfully executed. Saving record to persistence store with idempotency key: {0}", record.IdempotencyKey); await UpdateRecord(record); SaveToCache(record); } @@ -127,7 +120,6 @@ public virtual async Task SaveInProgress(JsonDocument data, DateTimeOffset now) null, GetHashedPayload(data) ); - Log.WriteDebug("saving in progress record for idempotency key: {0}", record.IdempotencyKey); await PutRecord(record, now); } @@ -140,7 +132,7 @@ public virtual async Task DeleteRecord(JsonDocument data, Exception throwable) { var idemPotencyKey = GetHashedIdempotencyKey(data); - Log.WriteDebug("Function raised an exception {0}. " + + Console.WriteLine("Function raised an exception {0}. " + "Clearing in progress record in persistence store for idempotency key: {1}", throwable.GetType().Name, idemPotencyKey); @@ -162,7 +154,6 @@ public virtual async Task GetRecord(JsonDocument data, DateTimeOffse var cachedRecord = RetrieveFromCache(idempotencyKey, now); if (cachedRecord != null) { - Log.WriteDebug("Idempotency record found in cache with idempotency key: {0}", idempotencyKey); ValidatePayload(data, cachedRecord); return cachedRecord; } @@ -217,7 +208,6 @@ private DataRecord RetrieveFromCache(string idempotencyKey, DateTimeOffset now) { return record; } - Log.WriteDebug("Removing expired local cache record for idempotency key: {0}", idempotencyKey); DeleteFromCache(idempotencyKey); } return null; @@ -285,7 +275,7 @@ private string GetHashedIdempotencyKey(JsonDocument data) { throw new IdempotencyKeyException("No data found to create a hashed idempotency key"); } - Log.WriteWarning("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyOptions.EventKeyJmesPath ?? string.Empty); + Console.WriteLine("No data found to create a hashed idempotency key. JMESPath: {0}", _idempotencyOptions.EventKeyJmesPath ?? string.Empty); } var hash = GenerateHash(node); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index b03e9f04..1f3aafab 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -123,8 +123,6 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) try { - Log.WriteDebug("Putting record for idempotency key: {0}", record.IdempotencyKey); - var expressionAttributeNames = new Dictionary { {"#id", _keyAttr}, @@ -146,7 +144,6 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) } catch (ConditionalCheckFailedException e) { - Log.WriteDebug("Failed to put record for already existing idempotency key: {0}", record.IdempotencyKey); throw new IdempotencyItemAlreadyExistsException( "Failed to put record for already existing idempotency key: " + record.IdempotencyKey, e); } @@ -156,7 +153,6 @@ public override async Task PutRecord(DataRecord record, DateTimeOffset now) /// public override async Task UpdateRecord(DataRecord record) { - Log.WriteDebug("Updating record for idempotency key: {0}", record.IdempotencyKey); var updateExpression = "SET #response_data = :response_data, #expiry = :expiry, #status = :status"; var expressionAttributeNames = new Dictionary @@ -194,7 +190,6 @@ public override async Task UpdateRecord(DataRecord record) /// public override async Task DeleteRecord(string idempotencyKey) { - Log.WriteDebug("Deleting record for idempotency key: {0}", idempotencyKey); var request = new DeleteItemRequest { TableName = _tableName, From 3606455630c3dde17e28e2eafcbce7d031acd857 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 30 May 2023 17:23:48 +0100 Subject: [PATCH 29/32] Add comments to private fields and methods --- .../AWS.Lambda.Powertools.Idempotency.csproj | 4 -- .../Idempotency.cs | 26 ++++++- .../IdempotencyOptions.cs | 10 +++ .../IdempotencyOptionsBuilder.cs | 21 ++++++ .../Internal/IdempotencyAspectHandler.cs | 24 ++++++- .../Internal/LRUCache.cs | 29 ++++++++ .../Persistence/BasePersistenceStore.cs | 33 +++++++++ .../Persistence/DataRecord.cs | 3 + .../Persistence/DynamoDBPersistenceStore.cs | 70 ++++++++++++++++++- 9 files changed, 212 insertions(+), 8 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj index f9445d14..684a6a50 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/AWS.Lambda.Powertools.Idempotency.csproj @@ -43,8 +43,4 @@ - - - - diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 2d4cda50..7bc6f26e 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -38,16 +38,27 @@ public sealed class Idempotency /// public BasePersistenceStore PersistenceStore { get; private set; } = null!; + /// + /// Idempotency Constructor + /// + /// internal Idempotency(IPowertoolsConfigurations powertoolsConfigurations) { powertoolsConfigurations.SetExecutionEnvironment(this); } - + /// + /// Set Idempotency options + /// + /// private void SetConfig(IdempotencyOptions options) { IdempotencyOptions = options; } + /// + /// Set Persistence Store + /// + /// private void SetPersistenceStore(BasePersistenceStore persistenceStore) { PersistenceStore = persistenceStore; @@ -79,10 +90,21 @@ public static void Configure(Action configurationAction) /// public class IdempotencyBuilder { + /// + /// Holds Idempotency options + /// private IdempotencyOptions _options; + /// + /// Persistence Store + /// private BasePersistenceStore _store; - + /// + /// Exposes Idempotency options + /// internal IdempotencyOptions Options => _options; + /// + /// Exposes Persistence Store + /// internal BasePersistenceStore Store => _store; /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs index 3cabe8f4..ed1b4a82 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptions.cs @@ -58,6 +58,16 @@ public class IdempotencyOptions /// public string HashFunction { get; } + /// + /// Constructor of . + /// + /// + /// + /// + /// + /// + /// + /// internal IdempotencyOptions( string eventKeyJmesPath, string payloadValidationJmesPath, diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs index daa3ef42..721ebf29 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotencyOptionsBuilder.cs @@ -7,12 +7,33 @@ namespace AWS.Lambda.Powertools.Idempotency; /// public class IdempotencyOptionsBuilder { + /// + /// Default maximum number of items in the local cache. + /// private readonly int _localCacheMaxItems = 256; + /// + /// Local cache enabled + /// private bool _useLocalCache; + /// + /// Default expiration in seconds. + /// private long _expirationInSeconds = 60 * 60; // 1 hour + /// + /// Event key JMESPath expression. + /// private string _eventKeyJmesPath; + /// + /// Payload validation JMESPath expression. + /// private string _payloadValidationJmesPath; + /// + /// Throw exception if no idempotency key is found. + /// private bool _throwOnNoIdempotencyKey; + /// + /// Default Hash function + /// private string _hashFunction = "MD5"; /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs index 76be618b..0124b3b6 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/IdempotencyAspectHandler.cs @@ -23,12 +23,29 @@ namespace AWS.Lambda.Powertools.Idempotency.Internal; internal class IdempotencyAspectHandler { + /// + /// Max retries + /// private const int MaxRetries = 2; - + /// + /// Delegate to execute the calling handler + /// private readonly Func> _target; + /// + /// Request payload + /// private readonly JsonDocument _data; + /// + /// Persistence store + /// private readonly BasePersistenceStore _persistenceStore; + /// + /// IdempotencyAspectHandler constructor + /// + /// + /// + /// public IdempotencyAspectHandler( Func> target, string functionName, @@ -170,6 +187,11 @@ private Task HandleForStatus(DataRecord record) } } + /// + /// Get the function's response and save it to the persistence layer + /// + /// Result from Handler delegate + /// private async Task GetFunctionResponse() { T response; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs index 741418ce..6dc95158 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs @@ -18,9 +18,24 @@ internal sealed class LRUCache /// private const int DefaultCapacity = 255; + /// + /// Shared synchronization object + /// private readonly object _lockObj = new(); + + /// + /// Maximum number of elements to cache. + /// private readonly int _capacity; + + /// + /// Dictionary to record the key and its data entry (O(1)) + /// private readonly Dictionary _cacheMap; + + /// + /// Linked list that tracks LRU items (O(1)) + /// private readonly LinkedList _cacheList; /// @@ -109,6 +124,10 @@ public void Set(TKey key, TValue value) } } + /// + /// Deletes the specified key and value to the cache. + /// + /// The key of the element to remove. public void Delete(TKey key) { lock (_lockObj) @@ -118,6 +137,9 @@ public void Delete(TKey key) } } + /// + /// Count of items in Cache + /// public int Count { get @@ -129,6 +151,10 @@ public int Count } } + /// + /// Move to most recent spot (head) in Linked List + /// + /// private void Touch(LinkedListNode node) { lock (_lockObj) @@ -141,6 +167,9 @@ private void Touch(LinkedListNode node) } } + /// + /// Linked List Element + /// private struct Entry { public readonly LinkedListNode Node; diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index a005032c..8cac2fe4 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -33,12 +33,23 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; /// public abstract class BasePersistenceStore : IPersistenceStore { + /// + /// Idempotency Options + /// private IdempotencyOptions _idempotencyOptions = null!; + + /// + /// Function name + /// private string _functionName; /// /// Boolean to indicate whether or not payload validation is enabled /// protected bool PayloadValidationEnabled; + + /// + /// LRUCache + /// private LRUCache _cache = null!; /// @@ -197,6 +208,12 @@ private void ValidatePayload(JsonDocument data, DataRecord dataRecord) } } + /// + /// Retrieve data record from cache + /// + /// Idempotency key + /// DateTime Offset + /// DataRecord instance private DataRecord RetrieveFromCache(string idempotencyKey, DateTimeOffset now) { if (!_idempotencyOptions.UseLocalCache) @@ -212,6 +229,11 @@ private DataRecord RetrieveFromCache(string idempotencyKey, DateTimeOffset now) } return null; } + + /// + /// Deletes item from cache + /// + /// private void DeleteFromCache(string idempotencyKey) { if (!_idempotencyOptions.UseLocalCache) @@ -282,6 +304,11 @@ private string GetHashedIdempotencyKey(JsonDocument data) return _functionName + "#" + hash; } + /// + /// Check if the provided data is missing an idempotency key. + /// + /// + /// True if the Idempotency key is missing private static bool IsMissingIdemPotencyKey(JsonElement data) { return data.ValueKind == JsonValueKind.Null || data.ValueKind == JsonValueKind.Undefined @@ -307,6 +334,12 @@ internal string GenerateHash(JsonElement data) return hash; } + /// + /// Get a hash of the provided string using the specified hash algorithm + /// + /// + /// + /// Hashed representation of the provided string private static string GetHash(HashAlgorithm hashAlgorithm, string input) { // Convert the input string to a byte array and compute the hash. diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs index 8f98fd35..acf23090 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs @@ -22,6 +22,9 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; /// public class DataRecord { + /// + /// Status + /// private readonly string _status; /// diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs index 1f3aafab..bec8f308 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DynamoDBPersistenceStore.cs @@ -30,16 +30,55 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; // ReSharper disable once InconsistentNaming public class DynamoDBPersistenceStore : BasePersistenceStore { + /// + /// DynamoDB table name + /// private readonly string _tableName; + /// + /// Key attribute + /// private readonly string _keyAttr; + /// + /// Static partition key value + /// private readonly string _staticPkValue; + /// + /// Sort key attribute + /// private readonly string _sortKeyAttr; + /// + /// Expiry attribute + /// private readonly string _expiryAttr; + /// + /// Status attribute + /// private readonly string _statusAttr; + /// + /// Data / Payload attribute + /// private readonly string _dataAttr; + /// + /// Validation attribute + /// private readonly string _validationAttr; + /// + /// DynamoDB client + /// private readonly AmazonDynamoDBClient _dynamoDbClient; + /// + /// Creates a new instance of . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// internal DynamoDBPersistenceStore(string tableName, string keyAttr, string staticPkValue, @@ -248,16 +287,45 @@ private Dictionary GetKey(string idempotencyKey) // ReSharper disable once InconsistentNaming public class DynamoDBPersistenceStoreBuilder { + /// + /// Lambda Function Name + /// private static readonly string FuncEnv = Environment.GetEnvironmentVariable(Constants.LambdaFunctionNameEnv); - + /// + /// DynamoDB table name + /// private string _tableName = null!; + /// + /// Key attribute + /// private string _keyAttr = "id"; + /// + /// Static partition key value + /// private string _staticPkValue = $"idempotency#{FuncEnv}"; + /// + /// Sort key attribute + /// private string _sortKeyAttr; + /// + /// Expiry attribute + /// private string _expiryAttr = "expiration"; + /// + /// Status attribute + /// private string _statusAttr = "status"; + /// + /// Data / Payload attribute + /// private string _dataAttr = "data"; + /// + /// Validation attribute + /// private string _validationAttr = "validation"; + /// + /// DynamoDB client + /// private AmazonDynamoDBClient _dynamoDbClient; /// From 74798111b80f7317acd63cd519b0232a58722a01 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Tue, 6 Jun 2023 12:45:56 +0100 Subject: [PATCH 30/32] Changes from feedback --- .../Idempotency.cs | 23 +++++++------------ .../IdempotentAttribute.cs | 7 ++++-- .../Persistence/BasePersistenceStore.cs | 2 +- .../Persistence/DataRecord.cs | 16 ++----------- .../Handlers/IdempotencyFunction.cs | 1 - 5 files changed, 16 insertions(+), 33 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs index 7bc6f26e..d81fb428 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Idempotency.cs @@ -90,22 +90,15 @@ public static void Configure(Action configurationAction) /// public class IdempotencyBuilder { - /// - /// Holds Idempotency options - /// - private IdempotencyOptions _options; - /// - /// Persistence Store - /// - private BasePersistenceStore _store; /// /// Exposes Idempotency options /// - internal IdempotencyOptions Options => _options; + internal IdempotencyOptions Options { get; private set; } + /// /// Exposes Persistence Store /// - internal BasePersistenceStore Store => _store; + internal BasePersistenceStore Store { get; private set; } /// /// Set the persistence layer to use for storing the request and response @@ -114,7 +107,7 @@ public class IdempotencyBuilder /// IdempotencyBuilder public IdempotencyBuilder WithPersistenceStore(BasePersistenceStore persistenceStore) { - _store = persistenceStore; + Store = persistenceStore; return this; } @@ -128,7 +121,7 @@ public IdempotencyBuilder UseDynamoDb(Action bu var builder = new DynamoDBPersistenceStoreBuilder(); builderAction(builder); - _store = builder.Build(); + Store = builder.Build(); return this; } @@ -141,7 +134,7 @@ public IdempotencyBuilder UseDynamoDb(string tableName) { var builder = new DynamoDBPersistenceStoreBuilder(); - _store = builder.WithTableName(tableName).Build(); + Store = builder.WithTableName(tableName).Build(); return this; } @@ -154,7 +147,7 @@ public IdempotencyBuilder WithOptions(Action builderA { var builder = new IdempotencyOptionsBuilder(); builderAction(builder); - _options = builder.Build(); + Options = builder.Build(); return this; } @@ -165,7 +158,7 @@ public IdempotencyBuilder WithOptions(Action builderA /// IdempotencyBuilder public IdempotencyBuilder WithOptions(IdempotencyOptions options) { - _options = options; + Options = options; return this; } } diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs index 2246c725..c14ca1af 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/IdempotentAttribute.cs @@ -14,6 +14,7 @@ */ using System; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using AspectInjector.Broker; @@ -73,7 +74,8 @@ protected internal sealed override T WrapSync(Func target, objec { return base.WrapSync(target, args, eventArgs); } - var payload = JsonDocument.Parse(JsonSerializer.Serialize(args[0])); + + var payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null; if (payload == null) { throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); @@ -105,7 +107,8 @@ protected internal sealed override async Task WrapAsync( { return await base.WrapAsync(target, args, eventArgs); } - var payload = JsonDocument.Parse(JsonSerializer.Serialize(args[0])); + + var payload = args is not null && args.Any() ? JsonDocument.Parse(JsonSerializer.Serialize(args[0])) : null; if (payload == null) { throw new IdempotencyConfigurationException("Unable to get payload from the method. Ensure there is at least one parameter or that you use @IdempotencyKey"); diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 8cac2fe4..937a3a05 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -29,7 +29,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Persistence; /// /// Persistence layer that will store the idempotency result. /// Base implementation. See for an implementation (default one) -/// Extend this class to use your own implementation (DocumentDB, Elasticache, ...) +/// Extend this class to use your own implementation (DocumentDB, ElastiCache, ...) /// public abstract class BasePersistenceStore : IPersistenceStore { diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs index acf23090..c35d9626 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/DataRecord.cs @@ -75,24 +75,12 @@ public bool IsExpired(DateTimeOffset now) { return ExpiryTimestamp != 0 && now.ToUnixTimeSeconds() > ExpiryTimestamp; } - /// /// Represents the Status /// - public DataRecordStatus Status - { - get - { - var now = DateTimeOffset.UtcNow; - if (IsExpired(now)) - { - return DataRecordStatus.EXPIRED; - } - - return Enum.Parse(_status); - } - } + public DataRecordStatus Status => + IsExpired(DateTimeOffset.UtcNow) ? DataRecordStatus.EXPIRED : Enum.Parse(_status); /// /// Determines whether the specified DataRecord is equal to the current DataRecord diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs index 832eb95a..66f0f8b3 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs @@ -35,7 +35,6 @@ public IdempotencyFunction(AmazonDynamoDBClient client) builder .WithOptions(optionsBuilder => optionsBuilder - //.WithUseLocalCache(true) .WithEventKeyJmesPath("powertools_json(Body).address") .WithExpiration(TimeSpan.FromSeconds(20))) .UseDynamoDb(storeBuilder => From ae021c67080f5d29a17dff74e5db20e26b08de12 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Wed, 7 Jun 2023 09:50:41 +0100 Subject: [PATCH 31/32] remove todo --- .../Persistence/BasePersistenceStore.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs index 937a3a05..8e10e632 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Persistence/BasePersistenceStore.cs @@ -66,8 +66,7 @@ public void Configure(IdempotencyOptions idempotencyOptions, string functionName _functionName += "." + functionName; } _idempotencyOptions = idempotencyOptions; - - //TODO: optimize to not reconfigure + if (!string.IsNullOrWhiteSpace(_idempotencyOptions.PayloadValidationJmesPath)) { PayloadValidationEnabled = true; From 33de0e163c5fcb9f9e34b1dc0b16eed814e86366 Mon Sep 17 00:00:00 2001 From: Henrique Graca <999396+hjgraca@users.noreply.github.com> Date: Wed, 7 Jun 2023 12:52:53 +0100 Subject: [PATCH 32/32] Addressing analysed warnings. --- .../AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs | 1 + .../Handlers/IdempotencyFunction.cs | 7 +++---- .../IdempotencyTest.cs | 7 ++----- .../Internal/LRUCacheTests.cs | 6 +++--- .../Persistence/DynamoDBFixture.cs | 1 - 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs index 6dc95158..3c6f898a 100644 --- a/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs +++ b/libraries/src/AWS.Lambda.Powertools.Idempotency/Internal/LRUCache.cs @@ -11,6 +11,7 @@ namespace AWS.Lambda.Powertools.Idempotency.Internal; /// /// The type of the key to the cached item. /// The type of the cached item. +// ReSharper disable once InconsistentNaming internal sealed class LRUCache { /// diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs index 66f0f8b3..5c500f05 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Handlers/IdempotencyFunction.cs @@ -21,7 +21,6 @@ using System.Threading.Tasks; using Amazon.DynamoDBv2; using Amazon.Lambda.APIGatewayEvents; -using Amazon.Lambda.Core; namespace AWS.Lambda.Powertools.Idempotency.Tests.Handlers; @@ -45,15 +44,15 @@ public IdempotencyFunction(AmazonDynamoDBClient client) } [Idempotent] - public async Task Handle(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + public async Task Handle(APIGatewayProxyRequest apigProxyEvent) { HandlerExecuted = true; - var result= await InternalFunctionHandler(apigProxyEvent,context); + var result= await InternalFunctionHandler(apigProxyEvent); return result; } - private async Task InternalFunctionHandler(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context) + private async Task InternalFunctionHandler(APIGatewayProxyRequest apigProxyEvent) { Dictionary headers = new() { diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs index f3c4d4ed..c0170682 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/IdempotencyTest.cs @@ -19,7 +19,6 @@ using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Amazon.Lambda.APIGatewayEvents; -using Amazon.Lambda.TestUtilities; using AWS.Lambda.Powertools.Idempotency.Tests.Handlers; using AWS.Lambda.Powertools.Idempotency.Tests.Persistence; using FluentAssertions; @@ -49,16 +48,14 @@ public async Task EndToEndTest() PropertyNameCaseInsensitive = true }; - //var persistenceStore = new InMemoryPersistenceStore(); - var context = new TestLambdaContext(); var request = JsonSerializer.Deserialize(await File.ReadAllTextAsync("./resources/apigw_event2.json"),options); - var response = await function.Handle(request, context); + var response = await function.Handle(request); function.HandlerExecuted.Should().BeTrue(); function.HandlerExecuted = false; - var response2 = await function.Handle(request, context); + var response2 = await function.Handle(request); function.HandlerExecuted.Should().BeFalse(); JsonSerializer.Serialize(response).Should().Be(JsonSerializer.Serialize(response)); diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs index 242e4522..54ec5388 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Internal/LRUCacheTests.cs @@ -21,6 +21,7 @@ //Source: https://github.dev/microsoft/botbuilder-dotnet/blob/main/tests/AdaptiveExpressions.Tests/LRUCacheTest.cs namespace AWS.Lambda.Powertools.Idempotency.Tests.Internal; +// ReSharper disable once InconsistentNaming public class LRUCacheTests { [Fact] @@ -58,7 +59,7 @@ public void TestDiacardPolicy() /* * The average time of this test is about 2ms. */ - public void TestDPMemorySmall() + public void TestDpMemorySmall() { var cache = new LRUCache(2); cache.Set(0, 1); @@ -87,7 +88,7 @@ public void TestDPMemorySmall() * The average time of this test is about 3ms. */ [Fact] - public void TestDPMemoryLarge() + public void TestDpMemoryLarge() { var cache = new LRUCache(500); cache.Set(0, 1); @@ -123,7 +124,6 @@ public async Task TestMultiThreadingAsync() const int numOfOps = 1000; for (var i = 0; i < numOfThreads; i++) { - var idx = i; tasks.Add(Task.Run(() => StoreElement(cache, numOfOps))); } diff --git a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs index ca37c459..5df377ee 100644 --- a/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs +++ b/libraries/tests/AWS.Lambda.Powertools.Idempotency.Tests/Persistence/DynamoDBFixture.cs @@ -18,7 +18,6 @@ using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; using Amazon.Runtime; -using AWS.Lambda.Powertools.Idempotency.Persistence; using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers;