Skip to content

Commit 63072ee

Browse files
docs(idempotency): add idempotency doc for CachePersistenceLayer (#3937)
Co-authored-by: Andrea Amorosi <dreamorosi@gmail.com>
1 parent 0205a87 commit 63072ee

12 files changed

+723
-36
lines changed

docs/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
FROM squidfunk/mkdocs-material@sha256:eb04b60c566a8862be6b553157c16a92fbbfc45d71b7e4e8593526aecca63f52
33

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

77
COPY requirements.txt /tmp/
88
RUN pip install --require-hashes -r /tmp/requirements.txt

docs/features/idempotency.md

Lines changed: 129 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The idempotency utility provides a simple solution to convert your Lambda functi
1212
* Select a subset of the event as the idempotency key using JMESPath expressions
1313
* Set a time window in which records with the same payload should be considered duplicates
1414
* Expires in-progress executions if the Lambda function times out halfway through
15+
* Support for Amazon DynamoDB, Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer
1516

1617
## Terminology
1718

@@ -49,40 +50,29 @@ classDiagram
4950

5051
## Getting started
5152

52-
### Installation
53+
!!! tip
54+
Throughout the documentation we use Amazon DynamoDB as the default persistence layer. If you prefer to use a cache based persistence layer, you can learn more in the [cache database](#cache-database) and [`CachePersistenceLayer`](#cachepersistencelayer) sections.
5355

54-
Install the library in your project
56+
### Amazon DynamoDB
5557

5658
```shell
5759
npm i @aws-lambda-powertools/idempotency @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
5860
```
5961

60-
While we support Amazon DynamoDB as a persistence layer out of the box, you need to bring your own AWS SDK for JavaScript v3 DynamoDB client.
62+
#### IAM Permissions
6163

62-
???+ note
63-
This utility supports **[AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/){target="_blank"} only**. If you are using the `nodejs18.x` runtime or newer, the AWS SDK for JavaScript v3 is already installed and you can install only the utility.
64-
65-
### IAM Permissions
66-
67-
Your Lambda function IAM Role must have `dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:UpdateItem` and `dynamodb:DeleteItem` IAM permissions before using this feature. If you're using one of our examples: [AWS Serverless Application Model (SAM)](#required-resources) or [Terraform](#required-resources) the required permissions are already included.
68-
69-
### Required resources
64+
Your Lambda function IAM Role must have `dynamodb:GetItem`, `dynamodb:PutItem`, `dynamodb:UpdateItem` and `dynamodb:DeleteItem` IAM permissions before using this feature. If you're using one of our examples below the required permissions are already included.
7065

71-
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.
66+
#### Table configuration
7267

73-
As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first.
74-
75-
**Default table configuration**
76-
77-
If you're not [changing the default configuration for the DynamoDB persistence layer](#dynamodbpersistencelayer), this is the expected default configuration:
68+
Unless you're looking to use an [existing table or customize each attribute](#dynamodbpersistencelayer), you only need the following:
7869

7970
| Configuration | Default value | Notes |
8071
| ------------------ | :------------ | -------------------------------------------------------------------------------------- |
8172
| Partition key | `id` | The id of each idempotency record which a combination of `functionName#hashOfPayload`. |
8273
| TTL attribute name | `expiration` | This can only be configured after your table is created if you're using AWS Console. |
8374

84-
???+ tip "Tip: You can share a single state table for all functions"
85-
You can reuse the same DynamoDB table to store idempotency state. We add the Lambda function name in addition to the idempotency key as a hash key.
75+
You **can** use a single DynamoDB table for all functions using this utility. We use the function name in addition to the idempotency key as a hash key.
8676

8777
=== "AWS Cloud Development Kit (CDK) example"
8878

@@ -102,17 +92,51 @@ If you're not [changing the default configuration for the DynamoDB persistence l
10292
--8<-- "examples/snippets/idempotency/templates/tableTerraform.tf"
10393
```
10494

105-
???+ warning "Warning: Large responses with DynamoDB persistence layer"
106-
When using this utility with DynamoDB, your function's responses must be [smaller than 400KB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-items){target="_blank"}.
95+
##### Limitations
96+
97+
* **DynamoDB restricts [item sizes to 400KB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-items)**. This means that your idempotent function's response must be smaller than 400KB, otherwise your function will fail. Consider using the [cache persistence layer](#cache-database) if you need to store larger responses.
98+
* **Expect 2 WCUs per non-idempotent call**. During the first invocation, we use `PutItem` for locking and `UpdateItem` for completion. Consider reviewing [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/) to estimate cost.
99+
* **Expect 1 RCU per idempotent calls**. On subsequent invocations, we use `PutItem` to optimistically attempt to lock the record using `ConditionExpression` and `ReturnValuesOnConditionCheckFailure` to return the record if it exists. This is a single read operation.
100+
101+
### Cache database
102+
103+
Depending on the persistence layer you want to use, install the library and the corresponding peer dependencies.
104+
105+
=== "Valkey"
106+
```shell
107+
npm i @aws-lambda-powertools/idempotency @valkey/valkey-glide
108+
```
109+
110+
=== "Redis OSS"
111+
```shell
112+
npm i @aws-lambda-powertools/idempotency @redis/client
113+
```
114+
115+
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"}.
107116

108-
Larger items cannot be written to DynamoDB and will cause exceptions.
117+
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 and permissions for writing and reading from the cache.
109118

110-
???+ info "Info: DynamoDB"
111-
Each function invocation will make only 1 request to DynamoDB by using DynamoDB's [conditional expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html){target="_blank"} to ensure that we don't overwrite existing records,
112-
and [ReturnValuesOnConditionCheckFailure](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValuesOnConditionCheckFailure){target="_blank"} to return the record if it exists.
113-
See [AWS Blog post on handling conditional write errors](https://aws.amazon.com/blogs/database/handle-conditional-write-errors-in-high-concurrency-scenarios-with-amazon-dynamodb/) for more details.
114-
For retried invocations, you will see 1WCU and 1RCU.
115-
Review the [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/){target="_blank"} to estimate the cost.
119+
#### Cache configuration
120+
121+
=== "AWS Cloud Development Kit (CDK) example"
122+
123+
```typescript title="template.ts" hl_lines="43"
124+
--8<-- "examples/snippets/idempotency/templates/cacheCdk.ts"
125+
```
126+
127+
1. Replace the VPC ID to match your VPC settings.
128+
2. Replace the Security Group ID to match your VPC settings.
129+
3. You can use the same template for Redis OSS, just replace the engine to `redis`.
130+
131+
=== "AWS Serverless Application Model (SAM) example"
132+
133+
```yaml hl_lines="6"
134+
--8<-- "examples/snippets/idempotency/templates/cacheSam.yml"
135+
```
136+
137+
1. You can use the same template for Redis OSS, just replace the engine to `redis`.
138+
2. Replace the Security Group ID and Subnet ID to match your VPC settings.
139+
3. Replace the Security Group ID and Subnet ID to match your VPC settings.
116140

117141
### MakeIdempotent function wrapper
118142

@@ -523,7 +547,29 @@ sequenceDiagram
523547
<i>Optional idempotency key</i>
524548
</center>
525549

526-
## Advanced
550+
#### Race condition with Cache
551+
552+
<center>
553+
```mermaid
554+
graph TD;
555+
A(Existing orphan record in cache)-->A1;
556+
A1[Two Lambda invoke at same time]-->B1[Lambda handler1];
557+
B1-->B2[Fetch from Cache];
558+
B2-->B3[Handler1 got orphan record];
559+
B3-->B4[Handler1 acquired lock];
560+
B4-->B5[Handler1 overwrite orphan record]
561+
B5-->B6[Handler1 continue to execution];
562+
A1-->C1[Lambda handler2];
563+
C1-->C2[Fetch from Cache];
564+
C2-->C3[Handler2 got orphan record];
565+
C3-->C4[Handler2 failed to acquire lock];
566+
C4-->C5[Handler2 wait and fetch from Cache];
567+
C5-->C6[Handler2 return without executing];
568+
B6-->D(Lambda handler executed only once);
569+
C6-->D;
570+
```
571+
<i>Race condition with Cache</i>
572+
</center>
527573

528574
### Persistence layers
529575

@@ -551,6 +597,44 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by
551597
| **sortKeyAttr** | | | Sort key of the table (if table is configured with a sort key). |
552598
| **staticPkValue** | | `idempotency#{LAMBDA_FUNCTION_NAME}` | Static value to use as the partition key. Only used when **sort_key_attr** is set. |
553599

600+
#### CachePersistenceLayer
601+
602+
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.
603+
604+
We recommend using [`@valkey/valkey-glide`](https://www.npmjs.com/package/@valkey/valkey-glide){target="_blank"} for Valkey or [`@redis/client`](https://www.npmjs.com/package/@redis/client){target="_blank"} for Redis. However, any Redis OSS-compatible client should work.
605+
606+
???+ info
607+
Make sure your cache client is configured and connected before using it with `CachePersistenceLayer`.
608+
609+
=== "Using Valkey Client"
610+
```typescript hl_lines="4 7-16 19"
611+
--8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts:5:"
612+
```
613+
614+
=== "Using Redis Client"
615+
```typescript hl_lines="4 7-10 13"
616+
--8<-- "examples/snippets/idempotency/cachePersistenceLayerRedis.ts:5:"
617+
```
618+
619+
When using Cache as a persistence layer, you can alter the attribute names by passing these parameters when initializing the persistence layer:
620+
621+
| Parameter | Required | Default | Description |
622+
| ------------------------ | ------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------- |
623+
| **client** | :heavy_check_mark: | | A connected Redis-compatible client instance |
624+
| **expiryAttr** | | `expiration` | Unix timestamp of when record expires |
625+
| **inProgressExpiryAttr** | | `in_progress_expiration` | Unix timestamp of when record expires while in progress (in case of the invocation times out) |
626+
| **statusAttr** | | `status` | Stores status of the lambda execution during and after invocation |
627+
| **dataAttr** | | `data` | Stores results of successfully executed Lambda handlers |
628+
| **validationKeyAttr** | | `validation` | Hashed representation of the parts of the event used for validation |
629+
630+
=== "Customizing CachePersistenceLayer"
631+
632+
```typescript hl_lines="20-24"
633+
--8<-- "examples/snippets/idempotency/customizeCachePersistenceLayer.ts:5:"
634+
```
635+
636+
## Advanced
637+
554638
### Customizing the default behavior
555639

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

948+
### Testing with local cache
949+
950+
Likewise, when using a cache database, you can use a local Valkey or Redis-OSS instance as a local server and replace the endpoint and port in the environment variables.
951+
952+
=== "valkeyHandler.test.ts"
953+
954+
```typescript hl_lines="5-8"
955+
--8<-- "examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts"
956+
```
957+
958+
=== "valkeyHandler.ts"
959+
960+
```typescript
961+
--8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts:5:"
962+
```
963+
864964
## Extra resources
865965

866966
If you're interested in a deep dive on how Amazon uses idempotency when building our APIs, check out
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
declare function processPayment(): Promise<{
2+
paymentId: string;
3+
}>;
4+
5+
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
6+
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
7+
import middy from '@middy/core';
8+
import { createClient } from '@redis/client';
9+
import type { Context } from 'aws-lambda';
10+
11+
const client = await createClient({
12+
url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
13+
username: 'default',
14+
}).connect();
15+
16+
const persistenceStore = new CachePersistenceLayer({
17+
client,
18+
});
19+
20+
export const handler = middy(async (_event: unknown, _context: Context) => {
21+
const payment = await processPayment();
22+
23+
return {
24+
paymentId: payment?.paymentId,
25+
message: 'success',
26+
statusCode: 200,
27+
};
28+
}).use(
29+
makeHandlerIdempotent({
30+
persistenceStore,
31+
})
32+
);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
declare function processPayment(): Promise<{
2+
paymentId: string;
3+
}>;
4+
5+
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
6+
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
7+
import middy from '@middy/core';
8+
import { GlideClient } from '@valkey/valkey-glide';
9+
import type { Context } from 'aws-lambda';
10+
11+
const client = await GlideClient.createClient({
12+
addresses: [
13+
{
14+
host: String(process.env.CACHE_ENDPOINT),
15+
port: Number(process.env.CACHE_PORT),
16+
},
17+
],
18+
useTLS: true,
19+
requestTimeout: 2000,
20+
});
21+
22+
const persistenceStore = new CachePersistenceLayer({
23+
client,
24+
});
25+
26+
export const handler = middy(async (_event: unknown, _context: Context) => {
27+
const payment = await processPayment();
28+
29+
return {
30+
paymentId: payment?.paymentId,
31+
message: 'success',
32+
statusCode: 200,
33+
};
34+
}).use(
35+
makeHandlerIdempotent({
36+
persistenceStore,
37+
})
38+
);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
declare function processPayment(): Promise<{
2+
paymentId: string;
3+
}>;
4+
5+
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
6+
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
7+
import middy from '@middy/core';
8+
import { GlideClient } from '@valkey/valkey-glide';
9+
import type { Context } from 'aws-lambda';
10+
11+
const client = await GlideClient.createClient({
12+
addresses: [
13+
{
14+
host: String(process.env.CACHE_ENDPOINT),
15+
port: Number(process.env.CACHE_PORT),
16+
},
17+
],
18+
useTLS: true,
19+
requestTimeout: 5000,
20+
});
21+
22+
const persistenceStore = new CachePersistenceLayer({
23+
client,
24+
expiryAttr: 'expiresAt',
25+
inProgressExpiryAttr: 'inProgressExpiresAt',
26+
statusAttr: 'currentStatus',
27+
dataAttr: 'resultData',
28+
validationKeyAttr: 'validationKey',
29+
});
30+
31+
export const handler = middy(async (_event: unknown, _context: Context) => {
32+
const payment = await processPayment();
33+
34+
return {
35+
paymentId: payment?.paymentId,
36+
message: 'success',
37+
statusCode: 200,
38+
};
39+
}).use(
40+
makeHandlerIdempotent({
41+
persistenceStore,
42+
})
43+
);

0 commit comments

Comments
 (0)