Skip to content

chore(v2): Add redis implementation to idempotency #1561

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,5 @@ example/HelloWorldFunction/build
.gradle
build/
.terraform*
terraform.tfstate*
terraform.tfstate*
/powertools-idempotency/powertools-idempotency-dynamodb/dynamodb-local-metadata.json
129 changes: 125 additions & 4 deletions docs/utilities/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 9 additions & 1 deletion powertools-e2e-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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://<ACCOUNTID>/<REGION>`

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:
Expand Down
72 changes: 72 additions & 0 deletions powertools-e2e-tests/handlers/idempotency-dynamodb/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>software.amazon.lambda</groupId>
<artifactId>e2e-test-handlers-parent</artifactId>
<version>1.0.0</version>
</parent>

<artifactId>e2e-test-handler-idempotency-dynamodb</artifactId>
<packaging>jar</packaging>
<name>A Lambda function using Powertools for AWS Lambda (Java) idempotency</name>

<dependencies>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-idempotency-dynamodb</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-logging</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>dev.aspectj</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<complianceLevel>${maven.compiler.target}</complianceLevel>
<aspectLibraries>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-idempotency-core</artifactId>
</aspectLibrary>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-logging</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
<version>2.0.0-SNAPSHOT</version>
</parent>

<artifactId>e2e-test-handler-idempotency</artifactId>
<artifactId>e2e-test-handler-idempotency-redis</artifactId>
<packaging>jar</packaging>
<name>A Lambda function using Powertools for AWS Lambda (Java) idempotency</name>

<dependencies>
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-idempotency</artifactId>
<artifactId>powertools-idempotency-redis</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.lambda</groupId>
Expand All @@ -30,7 +29,6 @@
<artifactId>aspectjrt</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
Expand All @@ -43,7 +41,7 @@
<aspectLibraries>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-idempotency</artifactId>
<artifactId>powertools-idempotency-core</artifactId>
</aspectLibrary>
<aspectLibrary>
<groupId>software.amazon.lambda</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Input, String> {
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());
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading