diff --git a/.github/workflows/pr_artifacts_size.yml b/.github/workflows/pr_artifacts_size.yml index 1d905af01..2244f7b06 100644 --- a/.github/workflows/pr_artifacts_size.yml +++ b/.github/workflows/pr_artifacts_size.yml @@ -11,7 +11,8 @@ on: - 'powertools-core/**' # not in v2 - 'powertools-common/**' # v2 only - 'powertools-e2e-tests/**' - - 'powertools-idempotency/**' + - 'powertools-idempotency-core/**' + - 'powertools-idempotency-dynamodb/**' - 'powertools-large-messages/**' - 'powertools-logging/**' - 'powertools-metrics/**' diff --git a/.gitignore b/.gitignore index 6615ac729..995d4ce8c 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,5 @@ example/HelloWorldFunction/build .gradle build/ .terraform* -terraform.tfstate* \ No newline at end of file +terraform.tfstate* +/powertools-idempotency/powertools-idempotency-dynamodb/dynamodb-local-metadata.json diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 5392b8d4c..85e3015ec 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -35,7 +35,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ... software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb {{ powertools.version }} ... @@ -56,7 +56,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb @@ -80,7 +80,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ... software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb {{ powertools.version }} ... @@ -101,7 +101,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb @@ -131,7 +131,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-idempotency:{{ powertools.version }}' + aspect 'software.amazon.lambda:powertools-idempotency-dynamodb:{{ powertools.version }}' } sourceCompatibility = 11 // or higher @@ -151,7 +151,7 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl } dependencies { - aspect 'software.amazon.lambda:powertools-idempotency:{{ powertools.version }}' + aspect 'software.amazon.lambda:powertools-idempotency-dynamodb:{{ powertools.version }}' } sourceCompatibility = 1.8 @@ -159,10 +159,12 @@ Depending on your version of Java (either Java 1.8 or 11+), the configuration sl ``` ### 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](https://aws.amazon.com/dynamodb/) and [Redis](https://redis.io/) are the supported persistnce layers. + +#### Using Amazon DynamoDB -As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first. +If you are using Amazon DynamoDB, you'll need to create a table. **Default table configuration** @@ -215,12 +217,65 @@ Resources: see 1WCU and 1RCU. Review the [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/) to estimate the cost. +#### Using Redis + +##### Redis resources + +You need an existing Redis service before setting up Redis as the persistent storage layer provider. You can also use Redis compatible services like [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/) or [Amazon MemoryDB for Redis](https://aws.amazon.com/memorydb/) as persistent storage layer provider. + +!!! tip "Tip:No existing Redis service?" + If you don't have an existing Redis service, we recommend using DynamoDB as persistent storage layer provider. DynamoDB does not require a VPC deployment and is easier to configure and operate. + +If you want to connect to a Redis cluster instead of a Standalone server, you need to enable Redis cluster mode by setting an AWS Lambda +environment variable `REDIS_CLUSTER_MODE` to `true` +In the following example, you can see a SAM template for deploying an AWS Lambda function by specifying the required environment variable. + +!!! warning "Warning: Large responses with Redis persistence layer" +When using this utility with Redis your function's responses must be smaller than 512MB. +Persisting larger items might cause exceptions. + +```yaml hl_lines="9" title="AWS Serverless Application Model (SAM) example" +Resources: + IdempotencyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: Function + Handler: helloworld.App::handleRequest + Environment: + Variables: + REDIS_CLUSTER_MODE: "true" +``` + +##### VPC Access +Your AWS Lambda Function must be able to reach the Redis endpoint before using it for idempotency persistent storage layer. In most cases you will need to [configure VPC access](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html) for your AWS Lambda Function. Using a public accessible Redis is not recommended. + +!!! tip "Amazon ElastiCache for Redis as persistent storage layer provider" + If you intend to use Amazon ElastiCache for Redis for idempotency persistent storage layer, you can also consult [this AWS tutorial](https://docs.aws.amazon.com/lambda/latest/dg/services-elasticache-tutorial.html). + +```yaml hl_lines="7-12" title="AWS Serverless Application Model (SAM) example" +Resources: + IdempotencyFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: Function + Handler: helloworld.App::handleRequest + VpcConfig: # (1)! + SecurityGroupIds: # (2)! + - sg-{your_sg_id} + SubnetIds: # (3)! + - subnet-{your_subnet_id_1} + - subnet-{your_subnet_id_2} +``` +1. Replace the Security Group ID and Subnet ID to match your Redis' VPC setting. +2. The security group ID or IDs of the VPC where the Redis is deployed. +3. The subnet IDs of the VPC where Redis is deployed. + ### Idempotent annotation -You can quickly start by initializing the `DynamoDBPersistenceStore` and using it with the `@Idempotent` annotation on your Lambda handler. +You can quickly start by initializing the persistence store used (e.g. `DynamoDBPersistenceStore` or `RedisPersistenceStore`) and using it with the `@Idempotent` annotation on your Lambda handler. !!! warning "Important" - Initialization and configuration of the `DynamoDBPersistenceStore` must be performed outside the handler, preferably in the constructor. + Initialization and configuration of the persistence store must be performed outside the handler, preferably in the constructor. === "App.java" @@ -635,6 +690,29 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by | **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. | +#### RedisPersistenceStore + +The redis persistence store has as a prerequisite to install a Redis datastore(https://redis.io/docs/about/) in either Standalone or Cluster mode. + +We are using [Redis hashes](https://redis.io/docs/data-types/hashes/) to store the idempotency fields and values. +There are some predefined fields that you can see listed in the following table. The predefined fields have some default values. + + +You can alter the field names by passing these parameters when initializing the persistence layer: + +| Parameter | Required | Default | Description | +|--------------------|----------|--------------------------------------|--------------------------------------------------------------------------------------------------------| +| **KeyPrefixName** | Y | `idempotency` | The redis hash key prefix | +| **KeyAttr** | Y | `id` | The redis hash key field name | +| **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 | + + +!!! Tip "Tip: You can share the same prefix and key for all functions" + You can reuse the same prefix and key to store idempotency state. We add your function name in addition to the idempotency key as a hash key. + ## Advanced ### Customizing the default behavior @@ -884,6 +962,49 @@ When creating the `DynamoDBPersistenceStore`, you can set a custom [`DynamoDbCli .build(); ``` +### Customizing Redis client + +The `RedisPersistenceStore` uses the [`JedisPooled`](https://www.javadoc.io/doc/redis.clients/jedis/latest/redis/clients/jedis/JedisPooled.html) java client to connect to the Redis standalone server or the [`JedisCluster`](https://javadoc.io/doc/redis.clients/jedis/4.0.0/redis/clients/jedis/JedisCluster.html) to connect to the Redis cluster. +When creating the `RedisPersistenceStore`, you can set a custom Jedis client: + +=== "Custom JedisPooled with connection timeout" + + ```java hl_lines="2-11 13 18" + public App() { + JedisConfig jedisConfig = JedisConfig.Builder.builder() + .withHost("redisHost") + .withPort("redisPort") + .withJedisClientConfig(DefaultJedisClientConfig.builder() + .user("user") + .password("secret") // leverage parameters-secrets module to retrieve this from Secrets Manager + .ssl(true) + .database(1) + .connectionTimeoutMillis(3000) + .build()) + .build(); + + JedisPooled jedisPooled = new JedisPooled(new HostAndPort("host",6789), jedisConfig); + + Idempotency.config().withPersistenceStore( + RedisPersistenceStore.builder() + .withKeyPrefixName("items-idempotency") + .withJedisClient(jedisPooled) + .build() + ).configure(); + } + ``` + +!!! info "Default configuration is the following:" + + ```java + DefaultJedisClientConfig.builder() + .user(null) + .password(null) + .ssl(false) + .database(0) + .build(); + ``` + ### 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. diff --git a/examples/powertools-examples-idempotency/pom.xml b/examples/powertools-examples-idempotency/pom.xml index 19d7a2272..39d6a8172 100644 --- a/examples/powertools-examples-idempotency/pom.xml +++ b/examples/powertools-examples-idempotency/pom.xml @@ -41,7 +41,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb ${project.version} @@ -95,7 +95,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb diff --git a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java index cf0c0ee31..0c4693230 100644 --- a/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java +++ b/examples/powertools-examples-idempotency/src/main/java/helloworld/App.java @@ -32,7 +32,7 @@ import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.utilities.JsonConfig; diff --git a/pom.xml b/pom.xml index a4df4116a..8261338d6 100644 --- a/pom.xml +++ b/pom.xml @@ -421,7 +421,6 @@ - process-sources compile test-compile diff --git a/powertools-batch/pom.xml b/powertools-batch/pom.xml index eaafdb56e..1886f56e6 100644 --- a/powertools-batch/pom.xml +++ b/powertools-batch/pom.xml @@ -25,6 +25,7 @@ powertools-batch + com.amazonaws diff --git a/powertools-e2e-tests/README.md b/powertools-e2e-tests/README.md index 61799e6f7..f41e16cd8 100644 --- a/powertools-e2e-tests/README.md +++ b/powertools-e2e-tests/README.md @@ -6,8 +6,16 @@ __Prerequisites__: ([credentials](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/credentials.html)). - [Java 11+](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/downloads-list.html) - [Docker](https://docs.docker.com/engine/install/) +- [CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install) -To execute the E2E tests, use the following command: `export JAVA_VERSION=11 && mvn clean verify -Pe2e` +### Execute test +Before executing the tests in a new AWS account, [bootstrap CDK](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.htmls) using the following command: + +`cdk bootstrap aws:///` + +To execute the E2E tests, use the following command: + +`export JAVA_VERSION=11 && mvn clean verify -Pe2e` ### Under the hood This module leverages the following components: diff --git a/powertools-e2e-tests/handlers/idempotency-dynamodb/pom.xml b/powertools-e2e-tests/handlers/idempotency-dynamodb/pom.xml new file mode 100644 index 000000000..b9d9fdb03 --- /dev/null +++ b/powertools-e2e-tests/handlers/idempotency-dynamodb/pom.xml @@ -0,0 +1,72 @@ + + 4.0.0 + + + software.amazon.lambda + e2e-test-handlers-parent + 1.0.0 + + + e2e-test-handler-idempotency-dynamodb + jar + A Lambda function using Powertools for AWS Lambda (Java) idempotency + + + + software.amazon.lambda + powertools-idempotency-dynamodb + + + software.amazon.lambda + powertools-logging + + + org.apache.logging.log4j + log4j-slf4j2-impl + + + com.amazonaws + aws-lambda-java-events + + + org.aspectj + aspectjrt + + + + + + + dev.aspectj + aspectj-maven-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-idempotency-core + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/e2e/Function.java similarity index 98% rename from powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java rename to powertools-e2e-tests/handlers/idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/e2e/Function.java index e4c2f2b9a..16109778d 100644 --- a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Function.java +++ b/powertools-e2e-tests/handlers/idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -27,7 +27,7 @@ import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; import software.amazon.lambda.powertools.logging.Logging; diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/e2e/Input.java similarity index 100% rename from powertools-e2e-tests/handlers/idempotency/src/main/java/software/amazon/lambda/powertools/e2e/Input.java rename to powertools-e2e-tests/handlers/idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/e2e/Input.java diff --git a/powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/idempotency-dynamodb/src/main/resources/log4j2.xml similarity index 100% rename from powertools-e2e-tests/handlers/idempotency/src/main/resources/log4j2.xml rename to powertools-e2e-tests/handlers/idempotency-dynamodb/src/main/resources/log4j2.xml diff --git a/powertools-e2e-tests/handlers/idempotency/pom.xml b/powertools-e2e-tests/handlers/idempotency-redis/pom.xml similarity index 92% rename from powertools-e2e-tests/handlers/idempotency/pom.xml rename to powertools-e2e-tests/handlers/idempotency-redis/pom.xml index da2bbfb80..9c0889028 100644 --- a/powertools-e2e-tests/handlers/idempotency/pom.xml +++ b/powertools-e2e-tests/handlers/idempotency-redis/pom.xml @@ -8,14 +8,13 @@ 2.0.0-SNAPSHOT - e2e-test-handler-idempotency + e2e-test-handler-idempotency-redis jar A Lambda function using Powertools for AWS Lambda (Java) idempotency - software.amazon.lambda - powertools-idempotency + powertools-idempotency-redis software.amazon.lambda @@ -30,7 +29,6 @@ aspectjrt - @@ -43,7 +41,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-core software.amazon.lambda diff --git a/powertools-e2e-tests/handlers/idempotency-redis/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/idempotency-redis/src/main/java/software/amazon/lambda/powertools/e2e/Function.java new file mode 100644 index 000000000..3b2f0d49e --- /dev/null +++ b/powertools-e2e-tests/handlers/idempotency-redis/src/main/java/software/amazon/lambda/powertools/e2e/Function.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.e2e; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.time.Duration; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.TimeZone; +import redis.clients.jedis.DefaultJedisClientConfig; +import software.amazon.lambda.powertools.idempotency.Idempotency; +import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; +import software.amazon.lambda.powertools.idempotency.Idempotent; +import software.amazon.lambda.powertools.idempotency.persistence.redis.JedisConfig; +import software.amazon.lambda.powertools.idempotency.persistence.redis.RedisPersistenceStore; +import software.amazon.lambda.powertools.logging.Logging; + + +public class Function implements RequestHandler { + public Function() { + Idempotency.config().withConfig( + IdempotencyConfig.builder() + .withExpiration(Duration.of(10, ChronoUnit.SECONDS)) + .build()) + .withPersistenceStore( + RedisPersistenceStore.builder() + .withJedisConfig(JedisConfig.Builder.builder() + .withJedisClientConfig(DefaultJedisClientConfig.builder().ssl(true).build()) + .withHost(System.getenv("REDIS_HOST")) + .withPort(6379).build()) + .build() + ).configure(); + } + + @Logging(logEvent = true) + @Idempotent + public String handleRequest(Input input, Context context) { + DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE_TIME.withZone(TimeZone.getTimeZone("UTC").toZoneId()); + return dtf.format(Instant.now()); + } +} \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/idempotency-redis/src/main/java/software/amazon/lambda/powertools/e2e/Input.java b/powertools-e2e-tests/handlers/idempotency-redis/src/main/java/software/amazon/lambda/powertools/e2e/Input.java new file mode 100644 index 000000000..e0e4c27c9 --- /dev/null +++ b/powertools-e2e-tests/handlers/idempotency-redis/src/main/java/software/amazon/lambda/powertools/e2e/Input.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.e2e; + +public class Input { + private String message; + + public Input(String message) { + this.message = message; + } + + public Input() { + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/powertools-e2e-tests/handlers/idempotency-redis/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/idempotency-redis/src/main/resources/log4j2.xml new file mode 100644 index 000000000..8925f70b9 --- /dev/null +++ b/powertools-e2e-tests/handlers/idempotency-redis/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml index 8cb2cb52c..f1f7ce597 100644 --- a/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml +++ b/powertools-e2e-tests/handlers/largemessage_idempotent/pom.xml @@ -15,7 +15,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb software.amazon.lambda @@ -47,7 +47,7 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb software.amazon.lambda diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml index 412593da9..b3b78d5aa 100644 --- a/powertools-e2e-tests/handlers/pom.xml +++ b/powertools-e2e-tests/handlers/pom.xml @@ -26,14 +26,12 @@ - batch - largemessage - largemessage_idempotent logging tracing metrics + idempotency-dynamodb + idempotency-redis batch - idempotency largemessage largemessage_idempotent parameters @@ -72,7 +70,12 @@ software.amazon.lambda - powertools-idempotency + powertools-idempotency-dynamodb + ${lambda.powertools.version} + + + software.amazon.lambda + powertools-idempotency-redis ${lambda.powertools.version} diff --git a/powertools-e2e-tests/pom.xml b/powertools-e2e-tests/pom.xml index 9d84ce9f2..09379d381 100644 --- a/powertools-e2e-tests/pom.xml +++ b/powertools-e2e-tests/pom.xml @@ -31,7 +31,7 @@ 1.8 1.8 10.3.0 - 2.109.0 + 2.115.0 @@ -41,6 +41,12 @@ test + + org.apache.logging.log4j + log4j-api + test + + software.amazon.awssdk lambda diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyDynamoDBE2ET.java similarity index 96% rename from powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java rename to powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyDynamoDBE2ET.java index 242d1a2db..1c9ac30b6 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyE2ET.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyDynamoDBE2ET.java @@ -29,7 +29,7 @@ import software.amazon.lambda.powertools.testutils.Infrastructure; import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; -public class IdempotencyE2ET { +public class IdempotencyDynamoDBE2ET { private static Infrastructure infrastructure; private static String functionName; @@ -38,7 +38,7 @@ public class IdempotencyE2ET { public static void setup() { String random = UUID.randomUUID().toString().substring(0, 6); infrastructure = Infrastructure.builder() - .testName(IdempotencyE2ET.class.getSimpleName()) + .testName(IdempotencyDynamoDBE2ET.class.getSimpleName()) .pathToFunction("idempotency") .idempotencyTable("idempo" + random) .build(); @@ -75,4 +75,4 @@ public void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws In Assertions.assertThat(result2.getResult()).isEqualTo(result1.getResult()); Assertions.assertThat(result3.getResult()).isNotEqualTo(result2.getResult()); } -} +} \ No newline at end of file diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyRedisE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyRedisE2ET.java new file mode 100644 index 000000000..412389741 --- /dev/null +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/IdempotencyRedisE2ET.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools; + +import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT; +import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction; + +import java.time.Year; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import software.amazon.lambda.powertools.testutils.Infrastructure; +import software.amazon.lambda.powertools.testutils.lambda.InvocationResult; + +public class IdempotencyRedisE2ET { + private static Infrastructure infrastructure; + private static String functionName; + + @BeforeAll + @Timeout(value = 15, unit = TimeUnit.MINUTES) + public static void setup() { + infrastructure = Infrastructure.builder() + .testName(IdempotencyRedisE2ET.class.getSimpleName()) + .redisDeployment(true) + .pathToFunction("idempotency-redis") + .build(); + Map outputs = infrastructure.deploy(); + functionName = outputs.get(FUNCTION_NAME_OUTPUT); + } + + @AfterAll + public static void tearDown() { + if (infrastructure != null) { + infrastructure.destroy(); + } + } + + @Test + public void test_ttlNotExpired_sameResult_ttlExpired_differentResult() throws InterruptedException { + // GIVEN + String event = "{\"message\":\"TTL 10sec\"}"; + + // WHEN + // First invocation + InvocationResult result1 = invokeFunction(functionName, event); + + // Second invocation (should get same result) + InvocationResult result2 = invokeFunction(functionName, event); + + Thread.sleep(12000); + + // Third invocation (should get different result) + InvocationResult result3 = invokeFunction(functionName, event); + + // THEN + Assertions.assertThat(result1.getResult()).contains(Year.now().toString()); + Assertions.assertThat(result2.getResult()).isEqualTo(result1.getResult()); + Assertions.assertThat(result3.getResult()).isNotEqualTo(result2.getResult()); + } +} diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java index 28a0f2bb4..e3a400dde 100644 --- a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java +++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/testutils/Infrastructure.java @@ -27,6 +27,7 @@ import java.util.Objects; import java.util.UUID; import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yaml.snakeyaml.Yaml; @@ -36,8 +37,10 @@ import software.amazon.awscdk.CfnOutput; import software.amazon.awscdk.DockerVolume; import software.amazon.awscdk.Duration; +import software.amazon.awscdk.Environment; import software.amazon.awscdk.RemovalPolicy; import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; import software.amazon.awscdk.cxapi.CloudAssembly; import software.amazon.awscdk.services.appconfig.CfnApplication; import software.amazon.awscdk.services.appconfig.CfnConfigurationProfile; @@ -45,7 +48,19 @@ import software.amazon.awscdk.services.appconfig.CfnDeploymentStrategy; import software.amazon.awscdk.services.appconfig.CfnEnvironment; import software.amazon.awscdk.services.appconfig.CfnHostedConfigurationVersion; -import software.amazon.awscdk.services.dynamodb.*; +import software.amazon.awscdk.services.dynamodb.Attribute; +import software.amazon.awscdk.services.dynamodb.AttributeType; +import software.amazon.awscdk.services.dynamodb.BillingMode; +import software.amazon.awscdk.services.dynamodb.StreamViewType; +import software.amazon.awscdk.services.dynamodb.Table; +import software.amazon.awscdk.services.ec2.IVpc; +import software.amazon.awscdk.services.ec2.Peer; +import software.amazon.awscdk.services.ec2.Port; +import software.amazon.awscdk.services.ec2.SecurityGroup; +import software.amazon.awscdk.services.ec2.SubnetSelection; +import software.amazon.awscdk.services.ec2.Vpc; +import software.amazon.awscdk.services.elasticache.CfnServerlessCache; +import software.amazon.awscdk.services.elasticache.CfnSubnetGroup; import software.amazon.awscdk.services.iam.PolicyStatement; import software.amazon.awscdk.services.kinesis.Stream; import software.amazon.awscdk.services.kinesis.StreamMode; @@ -114,10 +129,15 @@ public class Infrastructure { private final String queue; private final String kinesisStream; private final String largeMessagesBucket; + private IVpc vpc; private String ddbStreamsTableName; private String functionName; private Object cfnTemplate; private String cfnAssetDirectory; + private SubnetSelection subnetSelection; + private CfnSubnetGroup cfnSubnetGroup; + private SecurityGroup redisSecurityGroup; + private boolean isRedisDeployment = false; private Infrastructure(Builder builder) { this.stackName = builder.stackName; @@ -127,24 +147,52 @@ private Infrastructure(Builder builder) { this.timeout = builder.timeoutInSeconds; this.pathToFunction = builder.pathToFunction; this.idempotencyTable = builder.idemPotencyTable; + this.isRedisDeployment = builder.redisDeployment; this.appConfig = builder.appConfig; this.queue = builder.queue; this.kinesisStream = builder.kinesisStream; this.largeMessagesBucket = builder.largeMessagesBucket; this.ddbStreamsTableName = builder.ddbStreamsTableName; + this.region = Region.of(System.getProperty("AWS_DEFAULT_REGION", "eu-west-1")); this.app = new App(); - this.stack = createStackWithLambda(); - this.synthesize(); + this.stack = createStack(); this.httpClient = UrlConnectionHttpClient.builder().build(); - this.region = Region.of(System.getProperty("AWS_DEFAULT_REGION", "eu-west-1")); + this.account = StsClient.builder() .httpClient(httpClient) .region(region) .build().getCallerIdentity().account(); + if (isRedisDeployment) { + this.vpc = Vpc.Builder.create(this.stack, "PowertoolsVPC-" + stackName) + .availabilityZones(Arrays.asList(region.toString() + "a", region + "b")) + .build(); + + List subnets = vpc.getPublicSubnets().stream().map(subnet -> + subnet.getSubnetId()).collect(Collectors.toList()); + + redisSecurityGroup = SecurityGroup.Builder.create(stack, "ElastiCache-SG-" + stackName) + .vpc(vpc) + .allowAllOutbound(true) + .description("ElastiCache SecurityGroup") + .build(); + + cfnSubnetGroup = CfnSubnetGroup.Builder.create(stack, "Redis-Subnet-" + stackName) + .description("A subnet for the ElastiCache cluster") + .subnetIds(subnets).cacheSubnetGroupName("redis-SG-" + stackName).build(); + + subnetSelection = SubnetSelection.builder().subnets(vpc.getPublicSubnets()).build(); + } + + + createStackWithLambda(); + + this.synthesize(); + + s3 = S3Client.builder() .httpClient(httpClient) .region(region) @@ -201,9 +249,8 @@ public void destroy() { * * @return the CDK stack */ - private Stack createStackWithLambda() { + private void createStackWithLambda() { boolean createTableForAsyncTests = false; - Stack stack = new Stack(app, stackName); List packagingInstruction = Arrays.asList( "/bin/sh", @@ -230,14 +277,26 @@ private Stack createStackWithLambda() { .outputType(BundlingOutput.ARCHIVED); functionName = stackName + "-function"; - CfnOutput.Builder.create(stack, FUNCTION_NAME_OUTPUT) + CfnOutput.Builder.create(this.stack, FUNCTION_NAME_OUTPUT) .value(functionName) .build(); LOG.debug("Building Lambda function with command " + packagingInstruction.stream().collect(Collectors.joining(" ", "[", "]"))); - Function function = Function.Builder - .create(stack, functionName) + + if (isRedisDeployment) { + final SecurityGroup lambdaSecurityGroup = SecurityGroup.Builder.create(this.stack, "Lambda-SG") + .vpc(vpc) + .allowAllOutbound(true) + .description("Lambda SecurityGroup") + .build(); + redisSecurityGroup.addIngressRule(Peer.securityGroupId(lambdaSecurityGroup.getSecurityGroupId()), + Port.tcp(6379), + "Allow ElastiCache Server"); + } + + Function.Builder functionBuilder = Function.Builder + .create(this.stack, functionName) .code(Code.fromAsset("handlers/", AssetOptions.builder() .bundling(builderOptions .command(packagingInstruction) @@ -247,13 +306,22 @@ private Stack createStackWithLambda() { .handler("software.amazon.lambda.powertools.e2e.Function::handleRequest") .memorySize(1024) .timeout(Duration.seconds(timeout)) + .allowPublicSubnet(true) .runtime(runtime.getCdkRuntime()) .environment(envVar) - .tracing(tracing ? Tracing.ACTIVE : Tracing.DISABLED) - .build(); + .tracing(tracing ? Tracing.ACTIVE : Tracing.DISABLED); + + if (isRedisDeployment) { + functionBuilder.vpc(vpc) + .vpcSubnets(subnetSelection) + .securityGroups(singletonList(lambdaSecurityGroup)); + } + + Function function = functionBuilder.build(); + LogGroup.Builder - .create(stack, functionName + "-logs") + .create(this.stack, functionName + "-logs") .logGroupName("/aws/lambda/" + functionName) .retention(RetentionDays.ONE_DAY) .removalPolicy(RemovalPolicy.DESTROY) @@ -261,7 +329,7 @@ private Stack createStackWithLambda() { if (!StringUtils.isEmpty(idempotencyTable)) { Table table = Table.Builder - .create(stack, "IdempotencyTable") + .create(this.stack, "IdempotencyTable") .billingMode(BillingMode.PAY_PER_REQUEST) .removalPolicy(RemovalPolicy.DESTROY) .partitionKey(Attribute.builder().name("id").type(AttributeType.STRING).build()) @@ -273,9 +341,22 @@ private Stack createStackWithLambda() { table.grantReadWriteData(function); } + if (isRedisDeployment) { + List subnets = vpc.getPublicSubnets().stream().map(subnet -> + subnet.getSubnetId()).collect(Collectors.toList()); + CfnServerlessCache redisServer = CfnServerlessCache.Builder.create(this.stack, "ECC-" + stackName) + .serverlessCacheName("rc-" + stackName) + .engine("redis") + .subnetIds(subnets) + .securityGroupIds(singletonList(redisSecurityGroup.getSecurityGroupId())) + .build(); + + function.addEnvironment("REDIS_HOST", redisServer.getAtt("Endpoint.Address").toString()); + } + if (!StringUtils.isEmpty(queue)) { Queue sqsQueue = Queue.Builder - .create(stack, "SQSQueue") + .create(this.stack, "SQSQueue") .queueName(queue) .visibilityTimeout(Duration.seconds(timeout * 6)) .retentionPeriod(Duration.seconds(timeout * 6)) @@ -293,14 +374,14 @@ private Stack createStackWithLambda() { .build(); function.addEventSource(sqsEventSource); CfnOutput.Builder - .create(stack, "QueueURL") + .create(this.stack, "QueueURL") .value(sqsQueue.getQueueUrl()) .build(); createTableForAsyncTests = true; } if (!StringUtils.isEmpty(kinesisStream)) { Stream stream = Stream.Builder - .create(stack, "KinesisStream") + .create(this.stack, "KinesisStream") .streamMode(StreamMode.ON_DEMAND) .streamName(kinesisStream) .build(); @@ -316,13 +397,13 @@ private Stack createStackWithLambda() { .build(); function.addEventSource(kinesisEventSource); CfnOutput.Builder - .create(stack, "KinesisStreamName") + .create(this.stack, "KinesisStreamName") .value(stream.getStreamName()) .build(); } if (!StringUtils.isEmpty(ddbStreamsTableName)) { - Table ddbStreamsTable = Table.Builder.create(stack, "DDBStreamsTable") + Table ddbStreamsTable = Table.Builder.create(this.stack, "DDBStreamsTable") .tableName(ddbStreamsTableName) .stream(StreamViewType.KEYS_ONLY) .removalPolicy(RemovalPolicy.DESTROY) @@ -336,12 +417,12 @@ private Stack createStackWithLambda() { .reportBatchItemFailures(true) .build(); function.addEventSource(ddbEventSource); - CfnOutput.Builder.create(stack, "DdbStreamsTestTable").value(ddbStreamsTable.getTableName()).build(); + CfnOutput.Builder.create(this.stack, "DdbStreamsTestTable").value(ddbStreamsTable.getTableName()).build(); } if (!StringUtils.isEmpty(largeMessagesBucket)) { Bucket offloadBucket = Bucket.Builder - .create(stack, "LargeMessagesOffloadBucket") + .create(this.stack, "LargeMessagesOffloadBucket") .removalPolicy(RemovalPolicy.RETAIN) // autodelete does not work without cdk deploy .bucketName(largeMessagesBucket) .build(); @@ -352,19 +433,19 @@ private Stack createStackWithLambda() { if (appConfig != null) { CfnApplication app = CfnApplication.Builder - .create(stack, "AppConfigApp") + .create(this.stack, "AppConfigApp") .name(appConfig.getApplication()) .build(); CfnEnvironment environment = CfnEnvironment.Builder - .create(stack, "AppConfigEnvironment") + .create(this.stack, "AppConfigEnvironment") .applicationId(app.getRef()) .name(appConfig.getEnvironment()) .build(); // Create a fast deployment strategy, so we don't have to wait ages CfnDeploymentStrategy fastDeployment = CfnDeploymentStrategy.Builder - .create(stack, "AppConfigDeployment") + .create(this.stack, "AppConfigDeployment") .name("FastDeploymentStrategy") .deploymentDurationInMinutes(0) .finalBakeTimeInMinutes(0) @@ -383,14 +464,14 @@ private Stack createStackWithLambda() { CfnDeployment previousDeployment = null; for (Map.Entry entry : appConfig.getConfigurationValues().entrySet()) { CfnConfigurationProfile configProfile = CfnConfigurationProfile.Builder - .create(stack, "AppConfigProfileFor" + entry.getKey()) + .create(this.stack, "AppConfigProfileFor" + entry.getKey()) .applicationId(app.getRef()) .locationUri("hosted") .name(entry.getKey()) .build(); CfnHostedConfigurationVersion configVersion = CfnHostedConfigurationVersion.Builder - .create(stack, "AppConfigHostedVersionFor" + entry.getKey()) + .create(this.stack, "AppConfigHostedVersionFor" + entry.getKey()) .applicationId(app.getRef()) .contentType("text/plain") .configurationProfileId(configProfile.getRef()) @@ -398,7 +479,7 @@ private Stack createStackWithLambda() { .build(); CfnDeployment deployment = CfnDeployment.Builder - .create(stack, "AppConfigDepoymentFor" + entry.getKey()) + .create(this.stack, "AppConfigDepoymentFor" + entry.getKey()) .applicationId(app.getRef()) .environmentId(environment.getRef()) .deploymentStrategyId(fastDeployment.getRef()) @@ -415,7 +496,7 @@ private Stack createStackWithLambda() { } if (createTableForAsyncTests) { Table table = Table.Builder - .create(stack, "TableForAsyncTests") + .create(this.stack, "TableForAsyncTests") .billingMode(BillingMode.PAY_PER_REQUEST) .removalPolicy(RemovalPolicy.DESTROY) .partitionKey(Attribute.builder().name("functionName").type(AttributeType.STRING).build()) @@ -424,9 +505,17 @@ private Stack createStackWithLambda() { table.grantReadWriteData(function); function.addEnvironment("TABLE_FOR_ASYNC_TESTS", table.getTableName()); - CfnOutput.Builder.create(stack, "TableNameForAsyncTests").value(table.getTableName()).build(); + CfnOutput.Builder.create(this.stack, "TableNameForAsyncTests").value(table.getTableName()).build(); } + } + @NotNull + private Stack createStack() { + Stack stack = new Stack(app, stackName, StackProps.builder() + .env(Environment.builder() + .account(account) + .region(region.id()) + .build()).build()); return stack; } @@ -437,6 +526,7 @@ private void synthesize() { CloudAssembly synth = app.synth(); cfnTemplate = synth.getStackByName(stack.getStackName()).getTemplate(); cfnAssetDirectory = synth.getDirectory(); + } /** @@ -477,9 +567,9 @@ private Map findAssets() { String assetPath = file.get("source").get("path").asText(); String assetPackaging = file.get("source").get("packaging").asText(); String bucketName = - file.get("destinations").get("current_account-current_region").get("bucketName").asText(); + file.get("destinations").get("current_account-" + region.id()).get("bucketName").asText(); String objectKey = - file.get("destinations").get("current_account-current_region").get("objectKey").asText(); + file.get("destinations").get("current_account-" + region.id()).get("objectKey").asText(); Asset asset = new Asset(assetPath, assetPackaging, bucketName.replace("${AWS::AccountId}", account) .replace("${AWS::Region}", region.toString())); assets.put(objectKey, asset); @@ -504,6 +594,7 @@ public static class Builder { private String queue; private String kinesisStream; private String ddbStreamsTableName; + private boolean redisDeployment = false; private Builder() { runtime = mapRuntimeVersion("JAVA_VERSION"); @@ -562,6 +653,11 @@ public Builder idempotencyTable(String tableName) { return this; } + public Builder redisDeployment(boolean isRedisDeployment) { + this.redisDeployment = isRedisDeployment; + return this; + } + public Builder appConfig(AppConfig app) { this.appConfig = app; return this; diff --git a/powertools-idempotency/pom.xml b/powertools-idempotency/pom.xml index fd53cd9e2..581dbe961 100644 --- a/powertools-idempotency/pom.xml +++ b/powertools-idempotency/pom.xml @@ -25,13 +25,19 @@ powertools-idempotency - jar + pom Powertools for AWS Lambda (Java) - Idempotency + + powertools-idempotency-core + powertools-idempotency-dynamodb + powertools-idempotency-redis + + org.aspectj @@ -42,34 +48,6 @@ software.amazon.lambda powertools-common - - software.amazon.lambda - powertools-serialization - - - com.amazonaws - aws-lambda-java-core - - - software.amazon.awssdk - dynamodb - - - software.amazon.awssdk - netty-nio-client - - - software.amazon.awssdk - apache-client - - - - - software.amazon.awssdk - url-connection-client - ${aws.sdk.version} - - org.junit.jupiter @@ -86,6 +64,11 @@ junit-pioneer test + + org.aspectj + aspectjweaver + test + org.apache.commons commons-lang3 @@ -106,20 +89,6 @@ aws-lambda-java-tests test - - com.amazonaws - DynamoDBLocal - [1.12,2.0) - test - - - - io.github.ganadist.sqlite4java - libsqlite4java-osx-aarch64 - 1.0.392 - test - dylib - @@ -150,22 +119,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - software.amazon.awssdk.enhanced.dynamodb - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - \ No newline at end of file diff --git a/powertools-idempotency/powertools-idempotency-core/pom.xml b/powertools-idempotency/powertools-idempotency-core/pom.xml new file mode 100644 index 000000000..302cc24f5 --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-core/pom.xml @@ -0,0 +1,42 @@ + + + + + 4.0.0 + + + software.amazon.lambda + powertools-idempotency + 2.0.0-SNAPSHOT + + + powertools-idempotency-core + jar + + Powertools for AWS Lambda (Java) library Idempotency - Core + + Idempotency module common implementation + + + + + software.amazon.lambda + powertools-serialization + + + + \ No newline at end of file diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Constants.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotency.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java similarity index 99% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java index 58d0a7f5b..baf939d11 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyConfig.java @@ -92,7 +92,7 @@ public static class Builder { private int localCacheMaxItems = 256; private boolean useLocalCache = false; - private long expirationInSeconds = 60 * 60; // 1 hour + private long expirationInSeconds = 60 * 60L; // 1 hour private String eventKeyJMESPath; private String payloadValidationJMESPath; private boolean throwOnNoIdempotencyKey = false; diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/IdempotencyKey.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/Idempotent.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyAlreadyInProgressException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyConfigurationException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyInconsistentStateException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java similarity index 92% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java index ba7da69bf..42e17b5db 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemAlreadyExistsException.java @@ -27,4 +27,9 @@ public IdempotencyItemAlreadyExistsException() { public IdempotencyItemAlreadyExistsException(String msg, Throwable e) { super(msg, e); } + + public IdempotencyItemAlreadyExistsException(String msg) { + super(msg); + } + } diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyItemNotFoundException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyKeyException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyPersistenceLayerException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/exceptions/IdempotencyValidationException.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyHandler.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/IdempotentAspect.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCache.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java similarity index 96% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java index ac5044972..0a1acdf5c 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java +++ b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStore.java @@ -35,7 +35,6 @@ import java.util.stream.StreamSupport; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.amazon.awssdk.utils.StringUtils; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; @@ -55,7 +54,7 @@ public abstract class BasePersistenceStore implements PersistenceStore { protected boolean payloadValidationEnabled = false; private String functionName = ""; private boolean configured = false; - private long expirationInSeconds = 60 * 60; // 1 hour default + private long expirationInSeconds = 60 * 60L; // 1 hour default private boolean useLocalCache = false; private LRUCache cache; private String eventKeyJMESPath; @@ -64,6 +63,14 @@ public abstract class BasePersistenceStore implements PersistenceStore { private boolean throwOnNoIdempotencyKey = false; private String hashFunctionName; + private static boolean isEqual(String dataRecordPayload, String dataHash) { + if (dataHash != null && dataRecordPayload != null) { + return dataHash.length() != dataRecordPayload.length() ? false : dataHash.equals(dataRecordPayload); + } else { + return false; + } + } + /** * Initialize the base persistence layer from the configuration settings * @@ -73,7 +80,7 @@ public abstract class BasePersistenceStore implements PersistenceStore { public void configure(IdempotencyConfig config, String functionName) { String funcEnv = System.getenv(LAMBDA_FUNCTION_NAME_ENV); this.functionName = funcEnv != null ? funcEnv : "testFunction"; - if (!StringUtils.isEmpty(functionName)) { + if (functionName != null && !functionName.isEmpty()) { this.functionName += "." + functionName; } @@ -339,7 +346,7 @@ private MessageDigest getHashAlgorithm() { private void validatePayload(JsonNode data, DataRecord dataRecord) throws IdempotencyValidationException { if (payloadValidationEnabled) { String dataHash = getHashedPayload(data); - if (!StringUtils.equals(dataHash, dataRecord.getPayloadHash())) { + if (!isEqual(dataRecord.getPayloadHash(), dataHash)) { throw new IdempotencyValidationException("Payload does not match stored record for this event key"); } } diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DataRecord.java diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java b/powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java similarity index 100% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java rename to powertools-idempotency/powertools-idempotency-core/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/PersistenceStore.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyEnabledFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInternalKey.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionInvalid.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyInternalFunctionVoid.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyStringFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyWithErrorFunction.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/IdempotencyAspectTest.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/internal/cache/LRUCacheTest.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Basket.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/model/Product.java diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java similarity index 100% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java rename to powertools-idempotency/powertools-idempotency-core/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/BasePersistenceStoreTest.java diff --git a/powertools-idempotency/src/test/resources/apigw_event.json b/powertools-idempotency/powertools-idempotency-core/src/test/resources/apigw_event.json similarity index 100% rename from powertools-idempotency/src/test/resources/apigw_event.json rename to powertools-idempotency/powertools-idempotency-core/src/test/resources/apigw_event.json diff --git a/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml new file mode 100644 index 000000000..a8f69c468 --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-dynamodb/pom.xml @@ -0,0 +1,126 @@ + + + + + 4.0.0 + + + software.amazon.lambda + powertools-idempotency + 2.0.0-SNAPSHOT + + + powertools-idempotency-dynamodb + jar + + Powertools for AWS Lambda (Java) library Idempotency - DynamoDB + + DynamoDB implementation for the idempotency module + + + + + software.amazon.lambda + powertools-idempotency-core + ${project.version} + + + software.amazon.awssdk + url-connection-client + ${aws.sdk.version} + + + software.amazon.awssdk + dynamodb + + + software.amazon.awssdk + netty-nio-client + + + software.amazon.awssdk + apache-client + + + + + + + com.amazonaws + DynamoDBLocal + [1.12,2.0) + test + + + + io.github.ganadist.sqlite4java + libsqlite4java-osx-aarch64 + 1.0.392 + test + dylib + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + software.amazon.awssdk.enhanced.dynamodb + + + + + + dev.aspectj + aspectj-maven-plugin + ${aspectj-maven-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + ignore + ${project.build.sourceEncoding} + + + software.amazon.lambda + powertools-idempotency-core + + + + + + + compile + test-compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + \ No newline at end of file diff --git a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStore.java similarity index 98% rename from powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStore.java index 054f61ef3..40f9f93e7 100644 --- a/powertools-idempotency/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStore.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStore.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.idempotency.persistence; +package software.amazon.lambda.powertools.idempotency.persistence.dynamodb; import static software.amazon.lambda.powertools.common.internal.LambdaConstants.AWS_REGION_ENV; import static software.amazon.lambda.powertools.common.internal.LambdaConstants.LAMBDA_FUNCTION_NAME_ENV; @@ -39,11 +39,13 @@ import software.amazon.awssdk.services.dynamodb.model.GetItemResponse; import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; -import software.amazon.awssdk.utils.StringUtils; import software.amazon.lambda.powertools.common.internal.UserAgentConfigurator; import software.amazon.lambda.powertools.idempotency.Constants; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; +import software.amazon.lambda.powertools.idempotency.persistence.BasePersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; +import software.amazon.lambda.powertools.idempotency.persistence.PersistenceStore; /** * DynamoDB version of the {@link PersistenceStore}. Will store idempotency data in DynamoDB.
@@ -310,7 +312,7 @@ public static class Builder { * @return an instance of the {@link DynamoDBPersistenceStore} */ public DynamoDBPersistenceStore build() { - if (StringUtils.isEmpty(tableName)) { + if (tableName == null || "".equals(tableName)) { throw new IllegalArgumentException("Table name is not specified"); } return new DynamoDBPersistenceStore(tableName, keyAttr, staticPkValue, sortKeyAttr, expiryAttr, diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/DynamoDBConfig.java similarity index 98% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/DynamoDBConfig.java index 66ddb53ac..30b4976d7 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/DynamoDBConfig.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/DynamoDBConfig.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.idempotency; +package software.amazon.lambda.powertools.idempotency.dynamodb; import com.amazonaws.services.dynamodbv2.local.main.ServerRunner; import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer; diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/IdempotencyTest.java similarity index 93% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/IdempotencyTest.java index c94fec3db..be915b610 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/IdempotencyTest.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/IdempotencyTest.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.idempotency; +package software.amazon.lambda.powertools.idempotency.dynamodb; import static org.assertj.core.api.Assertions.assertThat; @@ -25,7 +25,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import software.amazon.awssdk.services.dynamodb.model.ScanRequest; -import software.amazon.lambda.powertools.idempotency.handlers.IdempotencyFunction; +import software.amazon.lambda.powertools.idempotency.dynamodb.handlers.IdempotencyFunction; public class IdempotencyTest extends DynamoDBConfig { diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/handlers/IdempotencyFunction.java similarity index 97% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/handlers/IdempotencyFunction.java index 43e191fc2..69b0e3177 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/handlers/IdempotencyFunction.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software.amazon.lambda.powertools.idempotency.dynamodb/handlers/IdempotencyFunction.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.idempotency.handlers; +package software.amazon.lambda.powertools.idempotency.dynamodb.handlers; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; @@ -31,7 +31,7 @@ import software.amazon.lambda.powertools.idempotency.Idempotency; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; import software.amazon.lambda.powertools.idempotency.Idempotent; -import software.amazon.lambda.powertools.idempotency.persistence.DynamoDBPersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.dynamodb.DynamoDBPersistenceStore; import software.amazon.lambda.powertools.utilities.JsonConfig; public class IdempotencyFunction implements RequestHandler { diff --git a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java similarity index 99% rename from powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java index b19cebfe1..e67420def 100644 --- a/powertools-idempotency/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/DynamoDBPersistenceStoreTest.java +++ b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/dynamodb/DynamoDBPersistenceStoreTest.java @@ -12,7 +12,7 @@ * */ -package software.amazon.lambda.powertools.idempotency.persistence; +package software.amazon.lambda.powertools.idempotency.persistence.dynamodb; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -39,10 +39,11 @@ import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType; import software.amazon.awssdk.services.dynamodb.model.ScanRequest; import software.amazon.lambda.powertools.idempotency.Constants; -import software.amazon.lambda.powertools.idempotency.DynamoDBConfig; import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; +import software.amazon.lambda.powertools.idempotency.dynamodb.DynamoDBConfig; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; /** * These test are using DynamoDBLocal and sqlite, see https://nickolasfisher.com/blog/Configuring-an-In-Memory-DynamoDB-instance-with-Java-for-Integration-Testing diff --git a/powertools-idempotency/src/test/resources/apigw_event2.json b/powertools-idempotency/powertools-idempotency-dynamodb/src/test/resources/apigw_event2.json similarity index 100% rename from powertools-idempotency/src/test/resources/apigw_event2.json rename to powertools-idempotency/powertools-idempotency-dynamodb/src/test/resources/apigw_event2.json diff --git a/powertools-idempotency/powertools-idempotency-redis/pom.xml b/powertools-idempotency/powertools-idempotency-redis/pom.xml new file mode 100644 index 000000000..69b89dd3b --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-redis/pom.xml @@ -0,0 +1,92 @@ + + + + + 4.0.0 + + software.amazon.lambda + powertools-idempotency + 2.0.0-SNAPSHOT + + + powertools-idempotency-redis + Powertools for AWS Lambda (Java) library Idempotency - Redis + + Redis implementation for the idempotency module + + + + + software.amazon.lambda + powertools-idempotency-core + ${project.version} + + + redis.clients + jedis + 5.1.0 + + + org.signal + embedded-redis + 0.8.3 + test + + + com.github.fppt + jedis-mock + 1.0.11 + + + + + + dev.aspectj + aspectj-maven-plugin + ${aspectj-maven-plugin.version} + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + ignore + ${project.build.sourceEncoding} + + + software.amazon.lambda + powertools-idempotency-core + + + + + + + compile + test-compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + + \ No newline at end of file diff --git a/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/Constants.java b/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/Constants.java new file mode 100644 index 000000000..06e14bee2 --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/Constants.java @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.idempotency.persistence.redis; + +final class Constants { + private Constants() { + throw new IllegalStateException("Utility class"); + } + + public static final String REDIS_CLUSTER_MODE = "REDIS_CLUSTER_MODE"; +} diff --git a/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/JedisConfig.java b/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/JedisConfig.java new file mode 100644 index 000000000..3b11078be --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/JedisConfig.java @@ -0,0 +1,91 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.idempotency.persistence.redis; + +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.JedisClientConfig; + +public class JedisConfig { + + private final String host; + private final Integer port; + private final JedisClientConfig jedisClientConfig; + + public JedisConfig(String host, Integer port, JedisClientConfig jedisClientConfig) { + this.host = host; + this.port = port; + this.jedisClientConfig = jedisClientConfig; + } + + String getHost() { + return host; + } + + Integer getPort() { + return port; + } + + public JedisClientConfig getJedisClientConfig() { + return jedisClientConfig; + } + + public static class Builder { + private String host = "localhost"; + private Integer port = 6379; + + private JedisClientConfig jedisClientConfig = DefaultJedisClientConfig.builder().build(); + + public static JedisConfig.Builder builder() { + return new JedisConfig.Builder(); + } + + public JedisConfig build() { + return new JedisConfig(host, port, jedisClientConfig); + } + + /** + * Host name of the redis deployment (optional), by default "localhost" + * + * @param host host name of the Redis deployment + * @return the builder instance (to chain operations) + */ + public JedisConfig.Builder withHost(String host) { + this.host = host; + return this; + } + + /** + * Port for the redis deployment (optional), by default 6379 + * + * @param port port for the Redis deployment + * @return the builder instance (to chain operations) + */ + public JedisConfig.Builder withPort(Integer port) { + this.port = port; + return this; + } + + /** + * Custom configuration for the redis client, by default {@link DefaultJedisClientConfig} + * + * @param jedisClientConfig custom configuration for the redis client + * @return the builder instance (to chain operations) + */ + public JedisConfig.Builder withJedisClientConfig(JedisClientConfig jedisClientConfig) { + this.jedisClientConfig = jedisClientConfig; + return this; + } + } +} diff --git a/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/RedisPersistenceStore.java b/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/RedisPersistenceStore.java new file mode 100644 index 000000000..60b83b212 --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-redis/src/main/java/software/amazon/lambda/powertools/idempotency/persistence/redis/RedisPersistenceStore.java @@ -0,0 +1,419 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.idempotency.persistence.redis; + +import static software.amazon.lambda.powertools.idempotency.persistence.DataRecord.Status.INPROGRESS; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalLong; +import java.util.stream.Collectors; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPooled; +import redis.clients.jedis.UnifiedJedis; +import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyConfigurationException; +import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; +import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; +import software.amazon.lambda.powertools.idempotency.persistence.BasePersistenceStore; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; +import software.amazon.lambda.powertools.idempotency.persistence.PersistenceStore; + +/** + * Redis version of the {@link PersistenceStore}. Stores idempotency data in Redis standalone or cluster mode.
+ * Use the {@link Builder} to create a new instance. + */ +public class RedisPersistenceStore extends BasePersistenceStore implements PersistenceStore { + + private static final Logger LOG = LoggerFactory.getLogger(RedisPersistenceStore.class); + public static final String UPDATE_SCRIPT_LUA = "putRecordOnCondition.lua"; + private final String keyPrefixName; + private final String keyAttr; + private final String expiryAttr; + private final String inProgressExpiryAttr; + private final String statusAttr; + private final String dataAttr; + private final String validationAttr; + private final UnifiedJedis jedisClient; + private final String luaScript; + private final JedisConfig jedisConfig; + + /** + * Private: use the {@link Builder} to instantiate a new {@link RedisPersistenceStore} + */ + private RedisPersistenceStore(JedisConfig jedisConfig, + String keyPrefixName, + String keyAttr, + String expiryAttr, + String inProgressExpiryAttr, + String statusAttr, + String dataAttr, + String validationAttr, + UnifiedJedis jedisClient) { + this.jedisConfig = jedisConfig; + this.keyPrefixName = keyPrefixName; + this.keyAttr = keyAttr; + this.expiryAttr = expiryAttr; + this.inProgressExpiryAttr = inProgressExpiryAttr; + this.statusAttr = statusAttr; + this.dataAttr = dataAttr; + this.validationAttr = validationAttr; + + if (jedisClient != null) { + this.jedisClient = jedisClient; + } else { + String idempotencyDisabledEnv = + System.getenv(software.amazon.lambda.powertools.idempotency.Constants.IDEMPOTENCY_DISABLED_ENV); + if (idempotencyDisabledEnv == null || "false".equalsIgnoreCase(idempotencyDisabledEnv)) { + this.jedisClient = getJedisClient(this.jedisConfig); + } else { + // we do not want to create a Jedis connection pool if idempotency is disabled + // null is ok as idempotency won't be called + this.jedisClient = null; + } + } + + luaScript = getLuaScript(); + } + + private String getLuaScript() { + final String luaScript; + try (InputStreamReader luaScriptReader = new InputStreamReader( + RedisPersistenceStore.class.getClassLoader().getResourceAsStream(UPDATE_SCRIPT_LUA), + StandardCharsets.UTF_8)) { + luaScript = new BufferedReader( + luaScriptReader).lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + throw new IdempotencyConfigurationException("Unable to load lua script with name " + UPDATE_SCRIPT_LUA); + } + return luaScript; + } + + public static Builder builder() { + return new Builder(); + } + + private static List getArgs(DataRecord dataRecord, Instant now) { + List args = new ArrayList<>(); + args.add(String.valueOf(now.getEpochSecond())); + args.add(String.valueOf(now.toEpochMilli())); + args.add(INPROGRESS.toString()); + args.add(String.valueOf(dataRecord.getExpiryTimestamp())); + args.add(dataRecord.getStatus().toString()); + return args; + } + + @Override + public DataRecord getRecord(String idempotencyKey) throws IdempotencyItemNotFoundException { + + String hashKey = getKey(idempotencyKey); + Map item = jedisClient.hgetAll(hashKey); + if (item.isEmpty()) { + throw new IdempotencyItemNotFoundException(idempotencyKey); + } + item.put(hashKey, idempotencyKey); + return itemToRecord(item, idempotencyKey); + } + + /** + * Store's the given idempotency dataRecord in the redis store. If there + * is an existing dataRecord that has expired - either due to the + * cache expiry or due to the in_progress_expiry - the dataRecord + * will be overwritten and the idempotent operation can continue. + * + * Note: This method writes only expiry and status information - not + * the results of the operation itself. + * + * @param dataRecord DataRecord instance to store + * @param now + * @throws IdempotencyItemAlreadyExistsException + */ + @Override + public void putRecord(DataRecord dataRecord, Instant now) { + + String inProgressExpiry = null; + if (dataRecord.getInProgressExpiryTimestamp().isPresent()) { + inProgressExpiry = String.valueOf(dataRecord.getInProgressExpiryTimestamp().getAsLong()); + } + + LOG.info("Putting dataRecord for idempotency key: {}", dataRecord.getIdempotencyKey()); + + Object execRes = putItemOnCondition(dataRecord, now, inProgressExpiry); + + if (execRes == null) { + String msg = String.format("Failed to put dataRecord for already existing idempotency key: %s", + getKey(dataRecord.getIdempotencyKey())); + LOG.info(msg); + throw new IdempotencyItemAlreadyExistsException(msg); + } else { + LOG.info("Record for idempotency key is set: {}", dataRecord.getIdempotencyKey()); + jedisClient.expireAt(getKey(dataRecord.getIdempotencyKey()), dataRecord.getExpiryTimestamp()); + } + } + + UnifiedJedis getJedisClient(JedisConfig jedisConfig) { + HostAndPort address = new HostAndPort(jedisConfig.getHost(), jedisConfig.getPort()); + JedisClientConfig config = jedisConfig.getJedisClientConfig(); + String isClusterMode = System.getenv(Constants.REDIS_CLUSTER_MODE); + if ("true".equalsIgnoreCase(isClusterMode)) { + return new JedisCluster(address, config, 5, new GenericObjectPoolConfig<>()); + } else { + return new JedisPooled(address, config); + } + } + + private Object putItemOnCondition(DataRecord dataRecord, Instant now, String inProgressExpiry) { + + List keys = getKeys(dataRecord); + + List args = getArgs(dataRecord, now); + + + if (inProgressExpiry != null) { + args.add(inProgressExpiry); + } + + return jedisClient.evalsha(jedisClient.scriptLoad(luaScript), keys, args); + } + + private List getKeys(DataRecord dataRecord) { + List keys = new ArrayList<>(); + String hashKey = getKey(dataRecord.getIdempotencyKey()); + keys.add(hashKey); + keys.add(prependField(hashKey, this.expiryAttr)); + keys.add(prependField(hashKey, this.statusAttr)); + keys.add(prependField(hashKey, this.inProgressExpiryAttr)); + return keys; + } + + @Override + public void updateRecord(DataRecord dataRecord) { + LOG.debug("Updating dataRecord for idempotency key: {}", dataRecord.getIdempotencyKey()); + String hashKey = getKey(dataRecord.getIdempotencyKey()); + + Map item = new HashMap<>(); + item.put(prependField(hashKey, this.dataAttr), dataRecord.getResponseData()); + item.put(prependField(hashKey, this.expiryAttr), String.valueOf(dataRecord.getExpiryTimestamp())); + item.put(prependField(hashKey, this.statusAttr), String.valueOf(dataRecord.getStatus().toString())); + + if (payloadValidationEnabled) { + item.put(prependField(hashKey, this.validationAttr), dataRecord.getPayloadHash()); + } + + jedisClient.hset(hashKey, item); + jedisClient.expireAt(hashKey, dataRecord.getExpiryTimestamp()); + } + + + @Override + public void deleteRecord(String idempotencyKey) { + LOG.debug("Deleting record for idempotency key: {}", idempotencyKey); + jedisClient.del(getKey(idempotencyKey)); + } + + /** + * Get the key to use for requests + * Sets a keyPrefixName for hash name and a keyAttr for hash key + * The key will be used in multi-key operations, therefore we need to + * include it into curly braces to instruct the redis cluster which part + * of the key will be used for hash and should be stored and looked-up in the same slot. + * + * @param idempotencyKey + * @return + * @see Redis Key distribution model + */ + private String getKey(String idempotencyKey) { + return "{" + this.keyPrefixName + ":" + this.keyAttr + ":" + idempotencyKey + "}"; + } + + /** + * Prepend each field key with the unique prefix that will be used for calculating the hash slot + * it will be stored in case of cluster mode Redis deployement + * + * @param hashKey + * @param field + * @return + * @see Redis Key distribution model + */ + private String prependField(String hashKey, String field) { + return hashKey + ":" + field; + } + + /** + * Translate raw item records from Redis to DataRecord + * + * @param item Item from redis response + * @return DataRecord instance + */ + private DataRecord itemToRecord(Map item, String idempotencyKey) { + String hashKey = getKey(idempotencyKey); + String prependedInProgressExpiryAttr = item.get(prependField(hashKey, this.inProgressExpiryAttr)); + return new DataRecord(item.get(getKey(idempotencyKey)), + DataRecord.Status.valueOf(item.get(prependField(hashKey, this.statusAttr))), + Long.parseLong(item.get(prependField(hashKey, this.expiryAttr))), + item.get(prependField(hashKey, this.dataAttr)), + item.get(prependField(hashKey, this.validationAttr)), + prependedInProgressExpiryAttr != null && !prependedInProgressExpiryAttr.isEmpty() ? + OptionalLong.of(Long.parseLong(prependedInProgressExpiryAttr)) : + OptionalLong.empty()); + } + + /** + * Use this builder to get an instance of {@link RedisPersistenceStore}.
+ * With this builder you can configure the characteristics of the Redis hash fields.
+ * You can also set a custom {@link UnifiedJedis} client. + */ + public static class Builder { + + private JedisConfig jedisConfig = JedisConfig.Builder.builder().build(); + private String keyPrefixName = "idempotency"; + private String keyAttr = "id"; + private String expiryAttr = "expiration"; + private String inProgressExpiryAttr = "in-progress-expiration"; + private String statusAttr = "status"; + private String dataAttr = "data"; + private String validationAttr = "validation"; + private UnifiedJedis jedisClient; + + /** + * Initialize and return a new instance of {@link RedisPersistenceStore}.
+ * Example:
+ *
+         *     RedisPersistenceStore.builder().withKeyAttr("uuid").build();
+         * 
+ * + * @return an instance of the {@link RedisPersistenceStore} + */ + public RedisPersistenceStore build() { + return new RedisPersistenceStore(jedisConfig, keyPrefixName, keyAttr, expiryAttr, + inProgressExpiryAttr, statusAttr, dataAttr, validationAttr, jedisClient); + } + + /** + * Redis prefix for the hash key (optional), by default "idempotency" + * + * @param keyPrefixName name of the key prefix + * @return the builder instance (to chain operations) + */ + public Builder withKeyPrefixName(String keyPrefixName) { + this.keyPrefixName = keyPrefixName; + return this; + } + + /** + * Redis name for hash key (optional), by default "id" + * + * @param keyAttr name of the key field of the hash + * @return the builder instance (to chain operations) + */ + public Builder withKeyAttr(String keyAttr) { + this.keyAttr = keyAttr; + return this; + } + + /** + * Redis field name for expiry timestamp (optional), by default "expiration" + * + * @param expiryAttr name of the expiry field in the hash + * @return the builder instance (to chain operations) + */ + public Builder withExpiryAttr(String expiryAttr) { + this.expiryAttr = expiryAttr; + return this; + } + + /** + * Redis field name for in progress expiry timestamp (optional), by default "in-progress-expiration" + * + * @param inProgressExpiryAttr name of the field in the hash + * @return the builder instance (to chain operations) + */ + public Builder withInProgressExpiryAttr(String inProgressExpiryAttr) { + this.inProgressExpiryAttr = inProgressExpiryAttr; + return this; + } + + /** + * Redis field name for status (optional), by default "status" + * + * @param statusAttr name of the status field in the hash + * @return the builder instance (to chain operations) + */ + public Builder withStatusAttr(String statusAttr) { + this.statusAttr = statusAttr; + return this; + } + + /** + * Redis field name for response data (optional), by default "data" + * + * @param dataAttr name of the data field in the hash + * @return the builder instance (to chain operations) + */ + public Builder withDataAttr(String dataAttr) { + this.dataAttr = dataAttr; + return this; + } + + /** + * Redis field name for validation (optional), by default "validation" + * + * @param validationAttr name of the validation field in the hash + * @return the builder instance (to chain operations) + */ + public Builder withValidationAttr(String validationAttr) { + this.validationAttr = validationAttr; + return this; + } + + /** + * Custom {@link UnifiedJedis} used to query Redis (optional).
+ * This will be cast to either {@link JedisPool} or {@link JedisCluster} + * depending on the mode of the Redis deployment and instructed by + * the value of {@link Constants#REDIS_CLUSTER_MODE} environment variable.
+ * + * @param jedisClient the {@link UnifiedJedis} instance to use + * @return the builder instance (to chain operations) + */ + public Builder withJedisClient(UnifiedJedis jedisClient) { + this.jedisClient = jedisClient; + return this; + } + + + /** + * Custom {@link JedisConfig} used to configure the Redis client(optional) + * + * @param jedisConfig + * @return the builder instance (to chain operations) + */ + public Builder withJedisConfig(JedisConfig jedisConfig) { + this.jedisConfig = jedisConfig; + return this; + } + } +} diff --git a/powertools-idempotency/powertools-idempotency-redis/src/main/resources/putRecordOnCondition.lua b/powertools-idempotency/powertools-idempotency-redis/src/main/resources/putRecordOnCondition.lua new file mode 100644 index 000000000..cbfd01ba0 --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-redis/src/main/resources/putRecordOnCondition.lua @@ -0,0 +1,19 @@ +local hashKey = KEYS[1] +local expiryKey = KEYS[2] +local statusKey = KEYS[3] +local inProgressExpiryKey = KEYS[4] +local timeNowSeconds = ARGV[1] +local timeNowMillis = ARGV[2] +local inProgressValue = ARGV[3] +local expiryValue = ARGV[4] +local statusValue = ARGV[5] +local inProgressExpiryValue = '' + +if ARGV[6] ~= nil then inProgressExpiryValue = ARGV[6] end; + +if redis.call('exists', hashKey) == 0 + or redis.call('hget', hashKey, expiryKey) < timeNowSeconds + or (redis.call('hexists', hashKey, inProgressExpiryKey) ~= 0 + and redis.call('hget', hashKey, inProgressExpiryKey) < timeNowMillis + and redis.call('hget', hashKey, statusKey) == inProgressValue) +then return redis.call('hset', hashKey, expiryKey, expiryValue, statusKey, statusValue, inProgressExpiryKey, inProgressExpiryValue) end; diff --git a/powertools-idempotency/powertools-idempotency-redis/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/redis/RedisPersistenceStoreTest.java b/powertools-idempotency/powertools-idempotency-redis/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/redis/RedisPersistenceStoreTest.java new file mode 100644 index 000000000..e65a58014 --- /dev/null +++ b/powertools-idempotency/powertools-idempotency-redis/src/test/java/software/amazon/lambda/powertools/idempotency/persistence/redis/RedisPersistenceStoreTest.java @@ -0,0 +1,448 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * Licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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. + * + */ + +package software.amazon.lambda.powertools.idempotency.persistence.redis; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.github.fppt.jedismock.server.ServiceOptions; +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; +import java.util.OptionalLong; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.SetEnvironmentVariable; +import redis.clients.jedis.DefaultJedisClientConfig; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPooled; +import redis.embedded.RedisServer; +import software.amazon.lambda.powertools.idempotency.IdempotencyConfig; +import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemAlreadyExistsException; +import software.amazon.lambda.powertools.idempotency.exceptions.IdempotencyItemNotFoundException; +import software.amazon.lambda.powertools.idempotency.persistence.DataRecord; + +@SetEnvironmentVariable(key = "REDIS_HOST", value = "localhost") +@SetEnvironmentVariable(key = "REDIS_PORT", value = "6379") +public class RedisPersistenceStoreTest { + static RedisServer redisServer; + private final JedisPooled jedisPool = new JedisPooled(); + private final RedisPersistenceStore redisPersistenceStore = RedisPersistenceStore.builder().build(); + + @BeforeAll + public static void init() { + + redisServer = RedisServer.builder().build(); + redisServer.start(); + } + + @AfterAll + public static void stop() { + redisServer.stop(); + } + + @Test + void putRecord_shouldCreateItemInRedis() { + Instant now = Instant.now(); + long ttl = 3600; + long expiry = now.plus(ttl, ChronoUnit.SECONDS).getEpochSecond(); + redisPersistenceStore.putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now); + + Map entry = jedisPool.hgetAll("{idempotency:id:key}"); + long ttlInRedis = jedisPool.ttl("{idempotency:id:key}"); + + assertThat(entry).isNotNull(); + assertThat(entry.get("{idempotency:id:key}:status")).isEqualTo("COMPLETED"); + assertThat(entry.get("{idempotency:id:key}:expiration")).isEqualTo(String.valueOf(expiry)); + assertThat(Math.round(ttlInRedis / 100.0) * 100).isEqualTo(ttl); + } + + @Test + void putRecord_shouldCreateItemInRedisWithCustomJedisConfig() { + + Instant now = Instant.now(); + long ttl = 3600; + long expiry = now.plus(ttl, ChronoUnit.SECONDS).getEpochSecond(); + RedisPersistenceStore store = new RedisPersistenceStore.Builder() + .withJedisConfig(JedisConfig.Builder.builder().withJedisClientConfig( + DefaultJedisClientConfig.builder().build()).build()) + .build(); + + store.putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now); + + Map entry = jedisPool.hgetAll("{idempotency:id:key}"); + long ttlInRedis = jedisPool.ttl("{idempotency:id:key}"); + + assertThat(entry).isNotNull(); + assertThat(entry.get("{idempotency:id:key}:status")).isEqualTo("COMPLETED"); + assertThat(entry.get("{idempotency:id:key}:expiration")).isEqualTo(String.valueOf(expiry)); + assertThat(Math.round(ttlInRedis / 100.0) * 100).isEqualTo(ttl); + } + + @Test + void putRecord_shouldCreateItemInRedisClusterMode() throws IOException { + com.github.fppt.jedismock.RedisServer redisCluster = com.github.fppt.jedismock.RedisServer + .newRedisServer() + .setOptions(ServiceOptions.defaultOptions().withClusterModeEnabled()) + .start(); + Instant now = Instant.now(); + long ttl = 3600; + long expiry = now.plus(ttl, ChronoUnit.SECONDS).getEpochSecond(); + JedisPooled jp = new JedisPooled(redisCluster.getHost(), redisCluster.getBindPort()); + RedisPersistenceStore store = new RedisPersistenceStore.Builder().withJedisClient(jp).build(); + + store.putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now); + + Map entry = jp.hgetAll("{idempotency:id:key}"); + long ttlInRedis = jp.ttl("{idempotency:id:key}"); + + assertThat(entry).isNotNull(); + assertThat(entry.get("{idempotency:id:key}:status")).isEqualTo("COMPLETED"); + assertThat(entry.get("{idempotency:id:key}:expiration")).isEqualTo(String.valueOf(expiry)); + assertThat(Math.round(ttlInRedis / 100.0) * 100).isEqualTo(ttl); + } + + @SetEnvironmentVariable(key = Constants.REDIS_CLUSTER_MODE, value = "true") + @Test + void putRecord_JedisClientInstanceOfJedisCluster() throws IOException { + com.github.fppt.jedismock.RedisServer redisCluster = com.github.fppt.jedismock.RedisServer + .newRedisServer() + .setOptions(ServiceOptions.defaultOptions().withClusterModeEnabled()) + .start(); + JedisConfig jedisConfig = JedisConfig.Builder.builder() + .withHost(redisCluster.getHost()) + .withPort(redisCluster.getBindPort()) + .withJedisClientConfig(DefaultJedisClientConfig.builder() + .user("default") + .password("") + .ssl(false) + .build()) + .build(); + assertThat(redisPersistenceStore.getJedisClient(jedisConfig) instanceof JedisCluster).isTrue(); + redisCluster.stop(); + } + + @SetEnvironmentVariable(key = Constants.REDIS_CLUSTER_MODE, value = "false") + @Test + void putRecord_JedisClientInstanceOfJedisPooled() { + JedisConfig jedisConfig = JedisConfig.Builder.builder() + .withHost(System.getenv("REDIS_HOST")) + .withPort(Integer.parseInt(System.getenv("REDIS_PORT"))) + .withJedisClientConfig(DefaultJedisClientConfig.builder().build()) + .build(); + assertThat(redisPersistenceStore.getJedisClient(jedisConfig) instanceof JedisCluster).isFalse(); + } + + @Test + void putRecord_shouldCreateItemInRedisWithInProgressExpiration() { + Instant now = Instant.now(); + long ttl = 3600; + long expiry = now.plus(ttl, ChronoUnit.SECONDS).getEpochSecond(); + OptionalLong progressExpiry = OptionalLong.of(now.minus(30, ChronoUnit.SECONDS).toEpochMilli()); + redisPersistenceStore.putRecord( + new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null, progressExpiry), now); + + Map redisItem = jedisPool.hgetAll("{idempotency:id:key}"); + long ttlInRedis = jedisPool.ttl("{idempotency:id:key}"); + + assertThat(redisItem).isNotNull(); + assertThat(redisItem).containsEntry("{idempotency:id:key}:status", "COMPLETED"); + assertThat(redisItem).containsEntry("{idempotency:id:key}:expiration", String.valueOf(expiry)); + assertThat(redisItem).containsEntry("{idempotency:id:key}:in-progress-expiration", + String.valueOf(progressExpiry.getAsLong())); + assertThat(Math.round(ttlInRedis / 100.0) * 100).isEqualTo(ttl); + } + + @Test + void putRecord_shouldCreateItemInRedis_withExistingJedisClient() { + Instant now = Instant.now(); + long expiry = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + RedisPersistenceStore store = new RedisPersistenceStore.Builder().withJedisClient(jedisPool).build(); + store.putRecord(new DataRecord("key", DataRecord.Status.COMPLETED, expiry, null, null), now); + + Map redisItem = jedisPool.hgetAll("{idempotency:id:key}"); + + assertThat(redisItem).isNotNull(); + assertThat(redisItem).containsEntry("{idempotency:id:key}:status", "COMPLETED"); + assertThat(redisItem).containsEntry("{idempotency:id:key}:expiration", String.valueOf(expiry)); + } + + @Test + void putRecord_shouldCreateItemInRedis_IfPreviousExpired() { + + Map item = new HashMap<>(); + Instant now = Instant.now(); + long expiry = now.minus(30, ChronoUnit.SECONDS).getEpochSecond(); + item.put("{idempotency:id:key}:expiration", String.valueOf(expiry)); + item.put("{idempotency:id:key}:status", DataRecord.Status.COMPLETED.toString()); + item.put("{idempotency:id:key}:data", "Fake Data"); + + long ttl = 3600; + long expiry2 = now.plus(ttl, ChronoUnit.SECONDS).getEpochSecond(); + jedisPool.hset("{idempotency:id:key}", item); + redisPersistenceStore.putRecord( + new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + null, + null + ), now); + + Map redisItem = jedisPool.hgetAll("{idempotency:id:key}"); + long ttlInRedis = jedisPool.ttl("{idempotency:id:key}"); + + assertThat(redisItem).isNotNull(); + assertThat(redisItem).containsEntry("{idempotency:id:key}:status", "INPROGRESS"); + assertThat(redisItem).containsEntry("{idempotency:id:key}:expiration", String.valueOf(expiry2)); + assertThat(Math.round(ttlInRedis / 100.0) * 100).isEqualTo(ttl); + } + + @Test + void putRecord_shouldCreateItemInRedis_IfLambdaWasInProgressAndTimedOut() { + + Map item = new HashMap<>(); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); + long progressExpiry = now.minus(30, ChronoUnit.SECONDS).toEpochMilli(); + item.put("{idempotency:id:key}:expiration", String.valueOf(expiry)); + item.put("{idempotency:id:key}:status", DataRecord.Status.INPROGRESS.toString()); + item.put("{idempotency:id:key}:data", "Fake Data"); + item.put("{idempotency:id:key}:in-progress-expiration", String.valueOf(progressExpiry)); + jedisPool.hset("{idempotency:id:key}", item); + + long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + redisPersistenceStore.putRecord( + new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + null, + null + ), now); + + Map redisItem = jedisPool.hgetAll("{idempotency:id:key}"); + + assertThat(redisItem).isNotNull(); + assertThat(redisItem).containsEntry("{idempotency:id:key}:status", "INPROGRESS"); + assertThat(redisItem).containsEntry("{idempotency:id:key}:expiration", String.valueOf(expiry2)); + } + + @Test + void putRecord_shouldThrowIdempotencyItemAlreadyExistsException_IfRecordAlreadyExist() { + + Map item = new HashMap<>(); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); + item.put("{idempotency:id:key}:expiration", String.valueOf(expiry)); // not expired + item.put("{idempotency:id:key}:status", DataRecord.Status.COMPLETED.toString()); + item.put("{idempotency:id:key}:data", "Fake Data"); + + jedisPool.hset("{idempotency:id:key}", item); + + long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + DataRecord dataRecord = new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + null, + null + ); + assertThatThrownBy(() -> { + redisPersistenceStore.putRecord( + dataRecord, now); + } + ).isInstanceOf(IdempotencyItemAlreadyExistsException.class); + + Map entry = jedisPool.hgetAll("{idempotency:id:key}"); + + assertThat(entry).isNotNull(); + assertThat(entry.get("{idempotency:id:key}:status")).isEqualTo("COMPLETED"); + assertThat(entry.get("{idempotency:id:key}:expiration")).isEqualTo(String.valueOf(expiry)); + assertThat(entry.get("{idempotency:id:key}:data")).isEqualTo("Fake Data"); + } + + @Test + void putRecord_shouldBlockUpdate_IfRecordAlreadyExistAndProgressNotExpiredAfterLambdaTimedOut() { + + Map item = new HashMap<>(); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); // not expired + long progressExpiry = now.plus(30, ChronoUnit.SECONDS).toEpochMilli(); // not expired + item.put("{idempotency:id:key}:expiration", String.valueOf(expiry)); + item.put("{idempotency:id:key}:status", DataRecord.Status.INPROGRESS.toString()); + item.put("{idempotency:id:key}:data", "Fake Data"); + item.put("{idempotency:id:key}:in-progress-expiration", String.valueOf(progressExpiry)); + jedisPool.hset("{idempotency:id:key}", item); + + long expiry2 = now.plus(3600, ChronoUnit.SECONDS).getEpochSecond(); + DataRecord dataRecord = new DataRecord("key", + DataRecord.Status.INPROGRESS, + expiry2, + "Fake Data 2", + null + ); + assertThatThrownBy(() -> redisPersistenceStore.putRecord( + dataRecord, now)) + .isInstanceOf(IdempotencyItemAlreadyExistsException.class); + + Map redisItem = jedisPool.hgetAll("{idempotency:id:key}"); + + assertThat(redisItem).isNotNull(); + assertThat(redisItem).containsEntry("{idempotency:id:key}:status", "INPROGRESS"); + assertThat(redisItem).containsEntry("{idempotency:id:key}:expiration", String.valueOf(expiry)); + assertThat(redisItem).containsEntry("{idempotency:id:key}:data", "Fake Data"); + } + + @Test + void getRecord_shouldReturnExistingRecord() throws IdempotencyItemNotFoundException { + + Map item = new HashMap<>(); + Instant now = Instant.now(); + long expiry = now.plus(30, ChronoUnit.SECONDS).getEpochSecond(); + item.put("{idempotency:id:key}:expiration", String.valueOf(expiry)); + item.put("{idempotency:id:key}:status", DataRecord.Status.COMPLETED.toString()); + item.put("{idempotency:id:key}:data", ("Fake Data")); + jedisPool.hset("{idempotency:id:key}", item); + + DataRecord record = redisPersistenceStore.getRecord("key"); + + assertThat(record.getIdempotencyKey()).isEqualTo("key"); + assertThat(record.getStatus()).isEqualTo(DataRecord.Status.COMPLETED); + assertThat(record.getResponseData()).isEqualTo("Fake Data"); + assertThat(record.getExpiryTimestamp()).isEqualTo(expiry); + } + + @Test + void getRecord_shouldThrowException_whenRecordIsAbsent() { + assertThatThrownBy(() -> redisPersistenceStore.getRecord("key")).isInstanceOf( + IdempotencyItemNotFoundException.class); + } + + @Test + void updateRecord_shouldUpdateRecord() { + Map item = new HashMap<>(); + Instant now = Instant.now(); + long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond(); + item.put("{idempotency:id:key}:expiration", String.valueOf(expiry)); + item.put("{idempotency:id:key}:status", DataRecord.Status.INPROGRESS.toString()); + jedisPool.hset("{idempotency:id:key}", item); + // enable payload validation + redisPersistenceStore.configure(IdempotencyConfig.builder().withPayloadValidationJMESPath("path").build(), + null); + + long ttl = 3600; + expiry = now.plus(ttl, ChronoUnit.SECONDS).getEpochSecond(); + DataRecord record = new DataRecord("key", DataRecord.Status.COMPLETED, expiry, "Fake result", "hash"); + redisPersistenceStore.updateRecord(record); + + Map redisItem = jedisPool.hgetAll("{idempotency:id:key}"); + long ttlInRedis = jedisPool.ttl("{idempotency:id:key}"); + + assertThat(redisItem).containsEntry("{idempotency:id:key}:status", "COMPLETED"); + assertThat(redisItem).containsEntry("{idempotency:id:key}:expiration", String.valueOf(expiry)); + assertThat(redisItem).containsEntry("{idempotency:id:key}:data", "Fake result"); + assertThat(redisItem).containsEntry("{idempotency:id:key}:validation", "hash"); + assertThat(Math.round(ttlInRedis / 100.0) * 100).isEqualTo(ttl); + } + + @Test + void deleteRecord_shouldDeleteRecord() { + Map item = new HashMap<>(); + Instant now = Instant.now(); + long expiry = now.plus(360, ChronoUnit.SECONDS).getEpochSecond(); + item.put("{idempotency:id:key}:expiration", String.valueOf(expiry)); + item.put("{idempotency:id:key}:status", DataRecord.Status.INPROGRESS.toString()); + jedisPool.hset("{idempotency:id:key}", item); + + redisPersistenceStore.deleteRecord("key"); + + Map items = jedisPool.hgetAll("{idempotency:id:key}"); + + assertThat(items).isEmpty(); + } + + + @Test + void endToEndWithCustomAttrNamesAndSortKey() throws IdempotencyItemNotFoundException { + try { + RedisPersistenceStore persistenceStore = RedisPersistenceStore.builder() + .withKeyPrefixName("items-idempotency") + .withJedisClient(jedisPool) + .withDataAttr("result") + .withExpiryAttr("expiry") + .withKeyAttr("key") + .withStatusAttr("state") + .withValidationAttr("valid") + .build(); + + Instant now = Instant.now(); + DataRecord record = new DataRecord( + "mykey", + DataRecord.Status.INPROGRESS, + now.plus(400, ChronoUnit.SECONDS).getEpochSecond(), + null, + null + ); + // PUT + persistenceStore.putRecord(record, now); + + Map redisItem = jedisPool.hgetAll("{items-idempotency:key:mykey}"); + + // GET + DataRecord recordInDb = persistenceStore.getRecord("mykey"); + + assertThat(redisItem).isNotNull(); + assertThat(redisItem).containsEntry("{items-idempotency:key:mykey}:state", + recordInDb.getStatus().toString()); + assertThat(redisItem).containsEntry("{items-idempotency:key:mykey}:expiry", + String.valueOf(recordInDb.getExpiryTimestamp())); + + // UPDATE + DataRecord updatedRecord = new DataRecord( + "mykey", + DataRecord.Status.COMPLETED, + now.plus(500, ChronoUnit.SECONDS).getEpochSecond(), + "response", + null + ); + persistenceStore.updateRecord(updatedRecord); + recordInDb = persistenceStore.getRecord("mykey"); + assertThat(recordInDb).isEqualTo(updatedRecord); + + // DELETE + persistenceStore.deleteRecord("mykey"); + assertThat(jedisPool.hgetAll("{items-idempotency:key:mykey}").size()).isZero(); + + } finally { + jedisPool.del("{items-idempotency:key:mykey}"); + } + } + + @Test + @SetEnvironmentVariable(key = software.amazon.lambda.powertools.idempotency.Constants.IDEMPOTENCY_DISABLED_ENV, value = "true") + void idempotencyDisabled_noClientShouldBeCreated() { + RedisPersistenceStore store = RedisPersistenceStore.builder().build(); + assertThatThrownBy(() -> store.getRecord("key")).isInstanceOf(NullPointerException.class); + } + + @AfterEach + void emptyDB() { + jedisPool.del("{idempotency:id:key}"); + } + +} diff --git a/powertools-large-messages/pom.xml b/powertools-large-messages/pom.xml index 4206183de..1bd670054 100644 --- a/powertools-large-messages/pom.xml +++ b/powertools-large-messages/pom.xml @@ -117,6 +117,11 @@ log4j-slf4j2-impl test
+ + org.apache.logging.log4j + log4j-api + test + diff --git a/powertools-logging/powertools-logging-log4j/pom.xml b/powertools-logging/powertools-logging-log4j/pom.xml index df6154560..752f8014c 100644 --- a/powertools-logging/powertools-logging-log4j/pom.xml +++ b/powertools-logging/powertools-logging-log4j/pom.xml @@ -35,6 +35,10 @@ org.apache.logging.log4j log4j-core + + org.apache.logging.log4j + log4j-api + org.apache.logging.log4j log4j-layout-template-json diff --git a/powertools-parameters/pom.xml b/powertools-parameters/pom.xml index 6c90e30a8..c730f4ca3 100644 --- a/powertools-parameters/pom.xml +++ b/powertools-parameters/pom.xml @@ -48,6 +48,10 @@ com.fasterxml.jackson.core jackson-databind + + org.aspectj + aspectjrt + org.junit.jupiter diff --git a/powertools-validation/pom.xml b/powertools-validation/pom.xml index a5fc4a890..4e0f2f436 100644 --- a/powertools-validation/pom.xml +++ b/powertools-validation/pom.xml @@ -66,6 +66,10 @@ com.amazonaws aws-lambda-java-serialization + + org.aspectj + aspectjrt + diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml index 747752130..f2e0bdd1c 100644 --- a/spotbugs-exclude.xml +++ b/spotbugs-exclude.xml @@ -81,6 +81,22 @@ + + + + + + + + + + + + + + + + @@ -127,9 +143,21 @@ - + + + + + + + + + + + + + @@ -231,4 +259,9 @@ + + + + +