Skip to content

Commit 1ae3de4

Browse files
committed
chore: reorg structure
1 parent 9c52a4f commit 1ae3de4

11 files changed

+502
-215
lines changed

docs/features/idempotency.md

Lines changed: 44 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -50,49 +50,20 @@ classDiagram
5050

5151
## Getting started
5252

53-
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).
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.
5455

55-
### Installation
56-
57-
Install the library in your project
56+
### Amazon DynamoDB
5857

5958
```shell
6059
npm i @aws-lambda-powertools/idempotency @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
6160
```
6261

63-
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.
64-
65-
???+ note
66-
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.
67-
68-
### IAM Permissions
69-
70-
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.
71-
72-
### Required resources
73-
74-
To start, you'll need:
75-
76-
<!-- markdownlint-disable MD030 -->
77-
78-
<div class="grid cards" markdown>
79-
* :octicons-database-16:{ .lg .middle } __Persistent storage__
80-
81-
---
82-
83-
[Amazon DynamoDB](#dynamodb-table) or [Cache](#cache-service)
84-
85-
* :simple-awslambda:{ .lg .middle } **AWS Lambda function**
86-
87-
---
62+
#### IAM Permissions
8863

89-
With permissions to use your persistent storage
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.
9065

91-
</div>
92-
93-
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.
94-
95-
#### DynamoDB table
66+
#### Table configuration
9667

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

@@ -101,8 +72,7 @@ Unless you're looking to use an [existing table or customize each attribute](#dy
10172
| Partition key | `id` | The id of each idempotency record which a combination of `functionName#hashOfPayload`. |
10273
| TTL attribute name | `expiration` | This can only be configured after your table is created if you're using AWS Console. |
10374

104-
???+ tip "Tip: You can share a single state table for all functions"
105-
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.
10676

10777
=== "AWS Cloud Development Kit (CDK) example"
10878

@@ -122,50 +92,51 @@ Unless you're looking to use an [existing table or customize each attribute](#dy
12292
--8<-- "examples/snippets/idempotency/templates/tableTerraform.tf"
12393
```
12494

125-
???+ warning "Warning: Large responses with DynamoDB persistence layer"
126-
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
12796

128-
Larger items cannot be written to DynamoDB and will cause exceptions.
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.
129100

130-
???+ info "Info: DynamoDB"
131-
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,
132-
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.
133-
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.
134-
For retried invocations, you will see 1WCU and 1RCU.
135-
Review the [DynamoDB pricing documentation](https://aws.amazon.com/dynamodb/pricing/){target="_blank"} to estimate the cost.
101+
### Cache database
136102

137-
#### Cache service
103+
Depending on the persistence layer you want to use, install the library and the corresponding peer dependencies.
138104

139-
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"}.
105+
=== "Valkey"
106+
```shell
107+
npm i @aws-lambda-powertools/idempotency @valkey/valkey-glide
108+
```
140109

141-
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.
110+
=== "Redis OSS"
111+
```shell
112+
npm i @aws-lambda-powertools/idempotency @redis/client
113+
```
142114

143-
##### Cache IaC examples
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"}.
144116

145-
!!! tip inline end "Prefer AWS Console/CLI?"
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.
146118

147-
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"}
119+
#### Cache configuration
148120

149-
=== "Valkey AWS CloudFormation example"
121+
=== "AWS Cloud Development Kit (CDK) example"
150122

151-
```yaml hl_lines="5 21"
152-
--8<-- "examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml"
123+
```typescript title="template.ts" hl_lines="43"
124+
--8<-- "examples/snippets/idempotency/templates/cacheCdk.ts"
153125
```
154126

155-
1. Replace the Security Group ID and Subnet ID to match your VPC settings.
156-
2. Replace the Security Group ID and Subnet ID to match your VPC settings.
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`.
157130

158-
=== "Redis AWS CloudFormation example"
131+
=== "AWS Serverless Application Model (SAM) example"
159132

160-
```yaml hl_lines="5 21"
161-
--8<-- "examples/snippets/idempotency/templates/redisServerlessCloudformation.yml"
133+
```yaml hl_lines="6"
134+
--8<-- "examples/snippets/idempotency/templates/cacheSam.yml"
162135
```
163136

164-
1. Replace the Security Group ID and Subnet ID to match your VPC settings.
137+
1. You can use the same template for Redis OSS, just replace the engine to `redis`.
165138
2. Replace the Security Group ID and Subnet ID to match your VPC settings.
166-
167-
168-
Once setup, you can find a quick start example for using a cache in [the persistent layers section](#cachepersistencelayer).
139+
3. Replace the Security Group ID and Subnet ID to match your VPC settings.
169140

170141
### MakeIdempotent function wrapper
171142

@@ -630,42 +601,36 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by
630601

631602
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.
632603

633-
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.
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.
634605

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

638609
=== "Using Valkey Client"
639-
```typescript hl_lines="9-18 21"
640-
--8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts"
610+
```typescript hl_lines="4 7-16 19"
611+
--8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts:5:"
641612
```
642613

643614
=== "Using Redis Client"
644-
```typescript hl_lines="9-12 15"
645-
--8<-- "examples/snippets/idempotency/cachePersistenceLayerRedis.ts"
615+
```typescript hl_lines="4 7-10 13"
616+
--8<-- "examples/snippets/idempotency/cachePersistenceLayerRedis.ts:5:"
646617
```
647618

648-
##### Cache attributes
649-
650619
When using Cache as a persistence layer, you can alter the attribute names by passing these parameters when initializing the persistence layer:
651620

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

661-
=== "Using Valkey"
662-
```typescript hl_lines="22-26"
663-
--8<-- "examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts"
664-
```
630+
=== "Customizing CachePersistenceLayer"
665631

666-
=== "Using Redis"
667-
```typescript hl_lines="16-20"
668-
--8<-- "examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts"
632+
```typescript hl_lines="20-24"
633+
--8<-- "examples/snippets/idempotency/customizeCachePersistenceLayer.ts:5:"
669634
```
670635

671636
## Advanced

examples/snippets/idempotency/cachePersistenceLayerRedis.ts

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
declare function processPayment(): Promise<{
2+
paymentId: string;
3+
}>;
4+
15
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
26
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
37
import middy from '@middy/core';
48
import { createClient } from '@redis/client';
59
import type { Context } from 'aws-lambda';
6-
import type { Request, Response } from './types.js';
710

8-
// Initialize the Redis client
911
const client = await createClient({
1012
url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
1113
username: 'default',
@@ -15,21 +17,15 @@ const persistenceStore = new CachePersistenceLayer({
1517
client,
1618
});
1719

18-
export const handler = middy(
19-
async (_event: Request, _context: Context): Promise<Response> => {
20-
try {
21-
// ... create payment
20+
export const handler = middy(async (_event: unknown, _context: Context) => {
21+
const payment = await processPayment();
2222

23-
return {
24-
paymentId: '1234567890',
25-
message: 'success',
26-
statusCode: 200,
27-
};
28-
} catch (error) {
29-
throw new Error('Error creating payment');
30-
}
31-
}
32-
).use(
23+
return {
24+
paymentId: payment?.paymentId,
25+
message: 'success',
26+
statusCode: 200,
27+
};
28+
}).use(
3329
makeHandlerIdempotent({
3430
persistenceStore,
3531
})

examples/snippets/idempotency/cachePersistenceLayerValkey.ts

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,37 @@
1+
declare function processPayment(): Promise<{
2+
paymentId: string;
3+
}>;
4+
15
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
26
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
37
import middy from '@middy/core';
48
import { GlideClient } from '@valkey/valkey-glide';
59
import type { Context } from 'aws-lambda';
6-
import type { Request, Response } from './types.js';
710

8-
// Initialize the Glide client
911
const client = await GlideClient.createClient({
1012
addresses: [
1113
{
12-
host: process.env.CACHE_ENDPOINT,
14+
host: String(process.env.CACHE_ENDPOINT),
1315
port: Number(process.env.CACHE_PORT),
1416
},
1517
],
1618
useTLS: true,
17-
requestTimeout: 5000,
19+
requestTimeout: 2000,
1820
});
1921

2022
const persistenceStore = new CachePersistenceLayer({
2123
client,
2224
});
2325

24-
export const handler = middy(
25-
async (_event: Request, _context: Context): Promise<Response> => {
26-
try {
27-
// ... create payment
26+
export const handler = middy(async (_event: unknown, _context: Context) => {
27+
const payment = await processPayment();
2828

29-
return {
30-
paymentId: '1234567890',
31-
message: 'success',
32-
statusCode: 200,
33-
};
34-
} catch (error) {
35-
throw new Error('Error creating payment');
36-
}
37-
}
38-
).use(
29+
return {
30+
paymentId: payment?.paymentId,
31+
message: 'success',
32+
statusCode: 200,
33+
};
34+
}).use(
3935
makeHandlerIdempotent({
4036
persistenceStore,
4137
})

examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts renamed to examples/snippets/idempotency/customizeCachePersistenceLayer.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1+
declare function processPayment(): Promise<{
2+
paymentId: string;
3+
}>;
4+
15
import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
26
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency/middleware';
37
import middy from '@middy/core';
48
import { GlideClient } from '@valkey/valkey-glide';
59
import type { Context } from 'aws-lambda';
6-
import type { Request, Response } from './types.js';
710

8-
// Initialize the Glide client
911
const client = await GlideClient.createClient({
1012
addresses: [
1113
{
12-
host: process.env.CACHE_ENDPOINT,
14+
host: String(process.env.CACHE_ENDPOINT),
1315
port: Number(process.env.CACHE_PORT),
1416
},
1517
],
@@ -26,21 +28,15 @@ const persistenceStore = new CachePersistenceLayer({
2628
validationKeyAttr: 'validationKey',
2729
});
2830

29-
export const handler = middy(
30-
async (_event: Request, _context: Context): Promise<Response> => {
31-
try {
32-
// ... create payment
31+
export const handler = middy(async (_event: unknown, _context: Context) => {
32+
const payment = await processPayment();
3333

34-
return {
35-
paymentId: '1234567890',
36-
message: 'success',
37-
statusCode: 200,
38-
};
39-
} catch (error) {
40-
throw new Error('Error creating payment');
41-
}
42-
}
43-
).use(
34+
return {
35+
paymentId: payment?.paymentId,
36+
message: 'success',
37+
statusCode: 200,
38+
};
39+
}).use(
4440
makeHandlerIdempotent({
4541
persistenceStore,
4642
})

examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)