Skip to content

docs(idempotency): add idempotency doc for CachePersistenceLayer #3937

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

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1237f1c
doc: add `requestTimeout` config option in Glide Client example
arnabrahman May 15, 2025
377e531
doc: cache persistence layer for idempotency
arnabrahman May 15, 2025
8b8c062
Merge branch 'main' into 3194-redis-idempotency-doc
arnabrahman May 16, 2025
f01034e
doc: fix the vpc note
arnabrahman May 16, 2025
7a17e63
doc: update VPC configuration comments in CloudFormation templates
arnabrahman May 16, 2025
4c1759d
doc: add examples for customizing cache persistence layer with Redis …
arnabrahman May 16, 2025
4656da4
doc: add examples for testing idempotency with local Redis cache
arnabrahman May 16, 2025
feead52
refactor: remove `CacheClient` type
arnabrahman May 16, 2025
624e9ad
fix: correct TypeScript syntax in Redis test example
arnabrahman May 16, 2025
fdd83cb
doc: add race condition diagram for Cache in idempotency documentation
arnabrahman May 16, 2025
9eadf2c
doc: update reference from Redis to Cache in idempotency documentation
arnabrahman May 16, 2025
fd56d7f
doc: remove redundancy in DynamoDB table creation instructions
arnabrahman May 16, 2025
3831cd2
doc: simplify instructions for creating a DynamoDB table in idempoten…
arnabrahman May 16, 2025
ad20002
doc: update references to Redis in idempotency documentation
arnabrahman May 16, 2025
fbcec70
doc: clarify language in getting started section and update DynamoDB …
arnabrahman May 16, 2025
99d25fa
doc: update references from cache database to cache service in idempo…
arnabrahman May 16, 2025
284c98e
doc: rephrase recommendation for starting with managed cache services…
arnabrahman May 16, 2025
4fe6d89
doc: update cache service recommendations to include Valkey and Redis…
arnabrahman May 16, 2025
fbabd78
doc: improve clarity in CachePersistenceLayer documentation and quick…
arnabrahman May 16, 2025
bd490c7
doc: add Valkey and Redis examples for local cache testing in idempot…
arnabrahman May 17, 2025
398b39c
doc: update reference from Redis to Cache in getting started section …
arnabrahman May 17, 2025
592e1a5
doc: correct casing for "Redis-compatible" in CachePersistenceLayer r…
arnabrahman May 17, 2025
9c52a4f
Merge branch 'main' into 3194-redis-idempotency-doc
arnabrahman May 17, 2025
3c20f59
Merge branch 'main' into 3194-redis-idempotency-doc
dreamorosi May 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
FROM squidfunk/mkdocs-material@sha256:eb04b60c566a8862be6b553157c16a92fbbfc45d71b7e4e8593526aecca63f52

# Install Node.js
RUN apk add --no-cache nodejs=20.15.1-r0 npm
RUN apk add --no-cache nodejs=22.13.1-r0 npm

COPY requirements.txt /tmp/
RUN pip install --require-hashes -r /tmp/requirements.txt
157 changes: 152 additions & 5 deletions docs/features/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ The idempotency utility provides a simple solution to convert your Lambda functi
* Select a subset of the event as the idempotency key using JMESPath expressions
* Set a time window in which records with the same payload should be considered duplicates
* Expires in-progress executions if the Lambda function times out halfway through
* Support for Amazon DynamoDB, Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer

## Terminology

Expand Down Expand Up @@ -49,6 +50,8 @@ classDiagram

## Getting started

We use Amazon DynamoDB as the default persistence layer in the documentation. If you prefer to use a cache based persistence layer, you can learn more from [this section](#cache-service).

### Installation

Install the library in your project
Expand All @@ -68,13 +71,30 @@ Your Lambda function IAM Role must have `dynamodb:GetItem`, `dynamodb:PutItem`,

### 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.
To start, you'll need:

<!-- markdownlint-disable MD030 -->

<div class="grid cards" markdown>
* :octicons-database-16:{ .lg .middle } __Persistent storage__

---

As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first.
[Amazon DynamoDB](#dynamodb-table) or [Cache](#cache-service)

**Default table configuration**
* :simple-awslambda:{ .lg .middle } **AWS Lambda function**

---

With permissions to use your persistent storage

</div>

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.

If you're not [changing the default configuration for the DynamoDB persistence layer](#dynamodbpersistencelayer), this is the expected default configuration:
#### DynamoDB table

Unless you're looking to use an [existing table or customize each attribute](#dynamodbpersistencelayer), you only need the following:

| Configuration | Default value | Notes |
| ------------------ | :------------ | -------------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -114,6 +134,39 @@ If you're not [changing the default configuration for the DynamoDB persistence l
For retried invocations, you will see 1WCU and 1RCU.
Review the [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/){target="_blank"} to estimate the cost.

#### Cache service

We recommend starting with a managed cache service, such as [Amazon ElastiCache for Valkey and for Redis OSS](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB](https://aws.amazon.com/memorydb/){target="_blank"}.

In both services, you'll need to configure [VPC access](https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html){target="_blank"} to your AWS Lambda.

##### Cache IaC examples

!!! tip inline end "Prefer AWS Console/CLI?"

Follow the official tutorials for [Amazon ElastiCache for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/LambdaRedis.html){target="_blank"} or [Amazon MemoryDB for Redis](https://aws.amazon.com/blogs/database/access-amazon-memorydb-for-redis-from-aws-lambda/){target="_blank"}

=== "Valkey AWS CloudFormation example"

```yaml hl_lines="5 21"
--8<-- "examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml"
```

1. Replace the Security Group ID and Subnet ID to match your VPC settings.
2. Replace the Security Group ID and Subnet ID to match your VPC settings.

=== "Redis AWS CloudFormation example"

```yaml hl_lines="5 21"
--8<-- "examples/snippets/idempotency/templates/redisServerlessCloudformation.yml"
```

1. Replace the Security Group ID and Subnet ID to match your VPC settings.
2. Replace the Security Group ID and Subnet ID to match your VPC settings.


Once setup, you can find a quick start example for using a cache in [the persistent layers section](#cachepersistencelayer).

### MakeIdempotent function wrapper

You can quickly start by initializing the `DynamoDBPersistenceLayer` class and using it with the `makeIdempotent` function wrapper on your Lambda handler.
Expand Down Expand Up @@ -523,7 +576,29 @@ sequenceDiagram
<i>Optional idempotency key</i>
</center>

## Advanced
#### Race condition with Cache

<center>
```mermaid
graph TD;
A(Existing orphan record in cache)-->A1;
A1[Two Lambda invoke at same time]-->B1[Lambda handler1];
B1-->B2[Fetch from Cache];
B2-->B3[Handler1 got orphan record];
B3-->B4[Handler1 acquired lock];
B4-->B5[Handler1 overwrite orphan record]
B5-->B6[Handler1 continue to execution];
A1-->C1[Lambda handler2];
C1-->C2[Fetch from Cache];
C2-->C3[Handler2 got orphan record];
C3-->C4[Handler2 failed to acquire lock];
C4-->C5[Handler2 wait and fetch from Cache];
C5-->C6[Handler2 return without executing];
B6-->D(Lambda handler executed only once);
C6-->D;
```
<i>Race condition with Cache</i>
</center>

### Persistence layers

Expand Down Expand Up @@ -551,6 +626,50 @@ 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 **sort_key_attr** is set. |

#### CachePersistenceLayer

The `CachePersistenceLayer` enables you to use Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer for idempotency state. You need to provide your own cache client.

We recommend using [valkey-glide](https://valkey.io/valkey-glide/#__tabbed_2_2){target="_blank"} for Valkey or [redis-client](https://www.npmjs.com/package/@redis/client){target="_blank"} for Redis. However, any Redis-compatible client can be used.

???+ info
Make sure your cache client is configured and connected before using it with `CachePersistenceLayer`.

=== "Using Valkey Client"
```typescript hl_lines="9-18 21"
--8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts"
```

=== "Using Redis Client"
```typescript hl_lines="9-12 15"
--8<-- "examples/snippets/idempotency/cachePersistenceLayerRedis.ts"
```

##### Cache attributes

When using Cache as a persistence layer, you can alter the attribute names by passing these parameters when initializing the persistence layer:

| Parameter | Required | Default | Description |
| ------------------------ | ------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| **client** | :heavy_check_mark: | | A connected Redis-compatible client instance |
| **expiryAttr** | | `expiration` | Unix timestamp of when record expires |
| **inProgressExpiryAttr** | | `in_progress_expiration` | Unix timestamp of when record expires while in progress (in case of the invocation times out) |
| **statusAttr** | | `status` | Stores status of the lambda execution during and after invocation |
| **dataAttr** | | `data` | Stores results of successfully executed Lambda handlers |
| **validationKeyAttr** | | `validation` | Hashed representation of the parts of the event used for validation |

=== "Using Valkey"
```typescript hl_lines="22-26"
--8<-- "examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts"
```

=== "Using Redis"
```typescript hl_lines="16-20"
--8<-- "examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts"
```

## Advanced

### Customizing the default behavior

Idempotent decorator can be further configured with **`IdempotencyConfig`** as seen in the previous examples. These are the available options for further configuration
Expand Down Expand Up @@ -861,6 +980,34 @@ When testing your Lambda function locally, you can use a local DynamoDB instance
--8<-- "examples/snippets/idempotency/workingWithLocalDynamoDB.ts"
```

### Testing with local Cache

When testing your Lambda function locally, you can use a local Valkey or Redis instance to test the idempotency feature. You can use [Valkey](https://valkey.io/topics/installation/){target="_blank"} or [Redis OSS](https://redis.io/docs/latest/get-started/){target="_blank"} as a local server.

=== "valkeyHandler.test.ts"

```typescript hl_lines="19-24"
--8<-- "examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts"
```

=== "valkeyHandler.ts"

```typescript
--8<-- "examples/snippets/idempotency/workingWithLocalCacheValkey.ts"
```

=== "redisHandler.test.ts"

```typescript hl_lines="19"
--8<-- "examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts"
```

=== "redisHandler.ts"

```typescript
--8<-- "examples/snippets/idempotency/workingWithLocalCacheRedis.ts"
```

## Extra resources

If you're interested in a deep dive on how Amazon uses idempotency when building our APIs, check out
Expand Down
36 changes: 36 additions & 0 deletions examples/snippets/idempotency/cachePersistenceLayerRedis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import middy from '@middy/core';
import { createClient } from '@redis/client';
import type { Context } from 'aws-lambda';
import type { Request, Response } from './types.js';

// Initialize the Redis client
const client = await createClient({
url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
username: 'default',
}).connect();

const persistenceStore = new CachePersistenceLayer({
client,
});

export const handler = middy(
async (_event: Request, _context: Context): Promise<Response> => {
try {
// ... create payment

return {
paymentId: '1234567890',
message: 'success',
statusCode: 200,
};
} catch (error) {
throw new Error('Error creating payment');
}
}
).use(
makeHandlerIdempotent({
persistenceStore,
})
);
42 changes: 42 additions & 0 deletions examples/snippets/idempotency/cachePersistenceLayerValkey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import middy from '@middy/core';
import { GlideClient } from '@valkey/valkey-glide';
import type { Context } from 'aws-lambda';
import type { Request, Response } from './types.js';

// Initialize the Glide client
const client = await GlideClient.createClient({
addresses: [
{
host: process.env.CACHE_ENDPOINT,
port: Number(process.env.CACHE_PORT),
},
],
useTLS: true,
requestTimeout: 5000,
});

const persistenceStore = new CachePersistenceLayer({
client,
});

export const handler = middy(
async (_event: Request, _context: Context): Promise<Response> => {
try {
// ... create payment

return {
paymentId: '1234567890',
message: 'success',
statusCode: 200,
};
} catch (error) {
throw new Error('Error creating payment');
}
}
).use(
makeHandlerIdempotent({
persistenceStore,
})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import middy from '@middy/core';
import { createClient } from '@redis/client';
import type { Context } from 'aws-lambda';
import type { Request, Response } from './types.js';

// Initialize the Redis client
const client = await createClient({
url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
username: 'default',
}).connect();

const persistenceStore = new CachePersistenceLayer({
client,
expiryAttr: 'expiresAt',
inProgressExpiryAttr: 'inProgressExpiresAt',
statusAttr: 'currentStatus',
dataAttr: 'resultData',
validationKeyAttr: 'validationKey',
});

export const handler = middy(
async (_event: Request, _context: Context): Promise<Response> => {
try {
// ... create payment

return {
paymentId: '1234567890',
message: 'success',
statusCode: 200,
};
} catch (error) {
throw new Error('Error creating payment');
}
}
).use(
makeHandlerIdempotent({
persistenceStore,
})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
import middy from '@middy/core';
import { GlideClient } from '@valkey/valkey-glide';
import type { Context } from 'aws-lambda';
import type { Request, Response } from './types.js';

// Initialize the Glide client
const client = await GlideClient.createClient({
addresses: [
{
host: process.env.CACHE_ENDPOINT,
port: Number(process.env.CACHE_PORT),
},
],
useTLS: true,
requestTimeout: 5000,
});

const persistenceStore = new CachePersistenceLayer({
client,
expiryAttr: 'expiresAt',
inProgressExpiryAttr: 'inProgressExpiresAt',
statusAttr: 'currentStatus',
dataAttr: 'resultData',
validationKeyAttr: 'validationKey',
});

export const handler = middy(
async (_event: Request, _context: Context): Promise<Response> => {
try {
// ... create payment

return {
paymentId: '1234567890',
message: 'success',
statusCode: 200,
};
} catch (error) {
throw new Error('Error creating payment');
}
}
).use(
makeHandlerIdempotent({
persistenceStore,
})
);
Loading