From 1237f1ccd24a7322e3daeaffee4fc74c145dd8fd Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 15 May 2025 22:09:31 +0600 Subject: [PATCH 01/24] doc: add `requestTimeout` config option in Glide Client example --- packages/idempotency/src/persistence/CachePersistenceLayer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/idempotency/src/persistence/CachePersistenceLayer.ts b/packages/idempotency/src/persistence/CachePersistenceLayer.ts index a8b2e8037..bc25bd04a 100644 --- a/packages/idempotency/src/persistence/CachePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/CachePersistenceLayer.ts @@ -43,6 +43,7 @@ import { IdempotencyRecord } from './IdempotencyRecord.js'; * port: Number(process.env.CACHE_PORT), * }], * useTLS: true, + * requestTimeout: 5000 * }); * * const persistence = new CachePersistenceLayer({ From 377e531a0de1474a8287ab8ae1860dfc45c6db14 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 15 May 2025 22:36:26 +0600 Subject: [PATCH 02/24] doc: cache persistence layer for idempotency --- docs/Dockerfile | 2 +- docs/features/idempotency.md | 85 ++++++++++++++++++- .../idempotency/cachePersistenceLayerRedis.ts | 36 ++++++++ .../cachePersistenceLayerValkey.ts | 42 +++++++++ .../redisServerlessCloudformation.yml | 31 +++++++ .../valkeyServerlessCloudformation.yml | 31 +++++++ 6 files changed, 224 insertions(+), 3 deletions(-) create mode 100644 examples/snippets/idempotency/cachePersistenceLayerRedis.ts create mode 100644 examples/snippets/idempotency/cachePersistenceLayerValkey.ts create mode 100644 examples/snippets/idempotency/templates/redisServerlessCloudformation.yml create mode 100644 examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml diff --git a/docs/Dockerfile b/docs/Dockerfile index c23fc4769..6dff74366 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -2,7 +2,7 @@ FROM squidfunk/mkdocs-material@sha256:95f2ff42251979c043d6cb5b1c82e6ae8189e57e02105813dd1ce124021a418b # 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 diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index be4addef7..53d9e47b8 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -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 @@ -49,6 +50,8 @@ classDiagram ## Getting started +We use Amazon DynamoDB as the default persistence layer in the documentation. If you prefer Cache, you can learn more from [this section](#cache-database). + ### Installation Install the library in your project @@ -68,8 +71,29 @@ Your Lambda function IAM Role must have `dynamodb:GetItem`, `dynamodb:PutItem`, ### Required resources +To start, you'll need: + + + +
+* :octicons-database-16:{ .lg .middle } __Persistent storage__ + + --- + + [Amazon DynamoDB](#dynamodb-table) or [Redis](#redis-database) + +* :simple-awslambda:{ .lg .middle } **AWS Lambda function** + + --- + + With permissions to use your persistent storage + +
+ 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. +#### DynamoDB table + As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first. **Default table configuration** @@ -114,6 +138,36 @@ 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 database + +We recommend you start with a Redis compatible management services such as [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB for Redis](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. + +=== "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. + +Once setup, you can find a quick start example for 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. @@ -523,8 +577,6 @@ sequenceDiagram Optional idempotency key -## Advanced - ### Persistence layers #### DynamoDBPersistenceLayer @@ -551,6 +603,35 @@ 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. To use it, initialize `CachePersistenceLayer` with a connected Redis-compatible client. + +=== "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 | + +## 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 diff --git a/examples/snippets/idempotency/cachePersistenceLayerRedis.ts b/examples/snippets/idempotency/cachePersistenceLayerRedis.ts new file mode 100644 index 000000000..ef3a073ca --- /dev/null +++ b/examples/snippets/idempotency/cachePersistenceLayerRedis.ts @@ -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 => { + try { + // ... create payment + + return { + paymentId: '1234567890', + message: 'success', + statusCode: 200, + }; + } catch (error) { + throw new Error('Error creating payment'); + } + } +).use( + makeHandlerIdempotent({ + persistenceStore, + }) +); diff --git a/examples/snippets/idempotency/cachePersistenceLayerValkey.ts b/examples/snippets/idempotency/cachePersistenceLayerValkey.ts new file mode 100644 index 000000000..3e9e1c080 --- /dev/null +++ b/examples/snippets/idempotency/cachePersistenceLayerValkey.ts @@ -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 => { + try { + // ... create payment + + return { + paymentId: '1234567890', + message: 'success', + statusCode: 200, + }; + } catch (error) { + throw new Error('Error creating payment'); + } + } +).use( + makeHandlerIdempotent({ + persistenceStore, + }) +); diff --git a/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml b/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml new file mode 100644 index 000000000..14f59c99c --- /dev/null +++ b/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml @@ -0,0 +1,31 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 + +Resources: + RedisServerlessIdempotency: + Type: AWS::ElastiCache::ServerlessCache + Properties: + Engine: redis + ServerlessCacheName: redis-cache + SecurityGroupIds: # (1)! + - security-{your_sg_id} + SubnetIds: + - subnet-{your_subnet_id_1} + - subnet-{your_subnet_id_2} + + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Runtime: nodejs22.x + Handler: index.js + VpcConfig: # (1)! + SecurityGroupIds: + - security-{your_sg_id} + SubnetIds: + - subnet-{your_subnet_id_1} + - subnet-{your_subnet_id_2} + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: sample + REDIS_HOST: !GetAtt RedisServerlessIdempotency.Endpoint.Address + REDIS_PORT: !GetAtt RedisServerlessIdempotency.Endpoint.Port \ No newline at end of file diff --git a/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml b/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml new file mode 100644 index 000000000..793eedf3e --- /dev/null +++ b/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml @@ -0,0 +1,31 @@ +AWSTemplateFormatVersion: "2010-09-09" +Transform: AWS::Serverless-2016-10-31 + +Resources: + ValkeyServerlessIdempotency: + Type: AWS::ElastiCache::ServerlessCache + Properties: + Engine: valkey + ServerlessCacheName: valkey-cache + SecurityGroupIds: # (1)! + - security-{your_sg_id} + SubnetIds: + - subnet-{your_subnet_id_1} + - subnet-{your_subnet_id_2} + + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + Runtime: nodejs22.x + Handler: index.js + VpcConfig: # (1)! + SecurityGroupIds: + - security-{your_sg_id} + SubnetIds: + - subnet-{your_subnet_id_1} + - subnet-{your_subnet_id_2} + Environment: + Variables: + POWERTOOLS_SERVICE_NAME: sample + VALKEY_HOST: !GetAtt ValkeyServerlessIdempotency.Endpoint.Address + VALKEY_PORT: !GetAtt ValkeyServerlessIdempotency.Endpoint.Port \ No newline at end of file From f01034e4f1ec0c18c11b2cbe443912ffff315293 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 11:25:42 +0600 Subject: [PATCH 03/24] doc: fix the vpc note --- docs/features/idempotency.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 53d9e47b8..918329b84 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -165,6 +165,7 @@ In both services, you'll need to configure [VPC access](https://docs.aws.amazon. ``` 1. Replace the Security Group ID and Subnet ID to match your VPC settings. + Once setup, you can find a quick start example for Cache in [the persistent layers section](#cachepersistencelayer). From 7a17e631b6ee038f4b2c54b72a86bab5e4dfddc7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 11:41:49 +0600 Subject: [PATCH 04/24] doc: update VPC configuration comments in CloudFormation templates --- docs/features/idempotency.md | 2 ++ .../idempotency/templates/redisServerlessCloudformation.yml | 2 +- .../idempotency/templates/valkeyServerlessCloudformation.yml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 918329b84..e6a6b0b3b 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -157,6 +157,7 @@ In both services, you'll need to configure [VPC access](https://docs.aws.amazon. ``` 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" @@ -165,6 +166,7 @@ In both services, you'll need to configure [VPC access](https://docs.aws.amazon. ``` 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 Cache in [the persistent layers section](#cachepersistencelayer). diff --git a/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml b/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml index 14f59c99c..1d995eedf 100644 --- a/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml +++ b/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml @@ -18,7 +18,7 @@ Resources: Properties: Runtime: nodejs22.x Handler: index.js - VpcConfig: # (1)! + VpcConfig: # (2)! SecurityGroupIds: - security-{your_sg_id} SubnetIds: diff --git a/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml b/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml index 793eedf3e..e1519712f 100644 --- a/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml +++ b/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml @@ -18,7 +18,7 @@ Resources: Properties: Runtime: nodejs22.x Handler: index.js - VpcConfig: # (1)! + VpcConfig: # (2)! SecurityGroupIds: - security-{your_sg_id} SubnetIds: From 4c1759d45dda4f3ab7c92e3adcbde7ea38add2e3 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 11:52:14 +0600 Subject: [PATCH 05/24] doc: add examples for customizing cache persistence layer with Redis and Valkey --- docs/features/idempotency.md | 10 ++++ .../customizeCachePersistenceLayerRedis.ts | 41 ++++++++++++++++ .../customizeCachePersistenceLayerValkey.ts | 47 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts create mode 100644 examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index e6a6b0b3b..f467f2355 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -633,6 +633,16 @@ When using Cache as a persistence layer, you can alter the attribute names by pa | **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 diff --git a/examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts b/examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts new file mode 100644 index 000000000..33a6398dc --- /dev/null +++ b/examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts @@ -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 => { + try { + // ... create payment + + return { + paymentId: '1234567890', + message: 'success', + statusCode: 200, + }; + } catch (error) { + throw new Error('Error creating payment'); + } + } +).use( + makeHandlerIdempotent({ + persistenceStore, + }) +); diff --git a/examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts b/examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts new file mode 100644 index 000000000..f166c151f --- /dev/null +++ b/examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts @@ -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 => { + try { + // ... create payment + + return { + paymentId: '1234567890', + message: 'success', + statusCode: 200, + }; + } catch (error) { + throw new Error('Error creating payment'); + } + } +).use( + makeHandlerIdempotent({ + persistenceStore, + }) +); From 4656da4c79bf19ba6683bfbb3671ac02ef6aaa6e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 16:17:46 +0600 Subject: [PATCH 06/24] doc: add examples for testing idempotency with local Redis cache --- docs/features/idempotency.md | 16 ++++++ .../workingWithLocalCacheRedis.test.ts | 51 +++++++++++++++++++ .../idempotency/workingWithLocalCacheRedis.ts | 31 +++++++++++ 3 files changed, 98 insertions(+) create mode 100644 examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts create mode 100644 examples/snippets/idempotency/workingWithLocalCacheRedis.ts diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index f467f2355..abd2db951 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -955,6 +955,22 @@ 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 redis instance to test the idempotency feature. You can use [Redis OSS](https://redis.io/docs/latest/get-started/){target="_blank"} or [Valkey](https://valkey.io/topics/installation/){target="_blank"} as a local server. + +=== "handler.test.ts" + + ```typescript hl_lines="20" + --8<-- "examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts" + ``` + +=== "handler.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 diff --git a/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts b/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts new file mode 100644 index 000000000..75e7291eb --- /dev/null +++ b/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts @@ -0,0 +1,51 @@ +import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; +import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; +import type { CacheClient } from '@aws-lambda-powertools/idempotency/cache/types'; +import { createClient } from '@redis/client'; +import type { Context } from 'aws-lambda'; +import { describe, expect, it } from 'vitest'; +import { handler } from './workingWithLocalDynamoDB.js'; + +const context = { + functionName: 'foo-bar-function', + memoryLimitInMB: '128', + invokedFunctionArn: + 'arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function', + awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e812345678', + getRemainingTimeInMillis: () => 1234, +} as Context; + +// Initialize the Redis client with local Redis server +const client = (await createClient({ + url: 'redis://localhost:6379', +}).connect()) as CacheClient; + +const mockPersistenceStore = new CachePersistenceLayer({ + client, +}); + +describe('Idempotent handler', () => { + it('returns the same response', async () => { + // Prepare + const idempotentHandler = makeIdempotent(handler, { + persistenceStore: mockPersistenceStore, + }); + + // Act + const response = await idempotentHandler( + { + foo: 'bar', + }, + context + ); + + // Assess + expect(response).toEqual({ + statusCode: 200, + body: JSON.stringify({ + paymentId: '123', + message: 'Payment created', + }), + }); + }); +}); diff --git a/examples/snippets/idempotency/workingWithLocalCacheRedis.ts b/examples/snippets/idempotency/workingWithLocalCacheRedis.ts new file mode 100644 index 000000000..fd749c818 --- /dev/null +++ b/examples/snippets/idempotency/workingWithLocalCacheRedis.ts @@ -0,0 +1,31 @@ +import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; +import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; +import type { CacheClient } from '@aws-lambda-powertools/idempotency/cache/types'; +import { createClient } from '@redis/client'; +import type { Context } from 'aws-lambda'; + +// Initialize the Redis client +const client = (await createClient({ + url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`, + username: 'default', +}).connect()) as CacheClient; + +const cachePersistenceStore = new CachePersistenceLayer({ + client, +}); + +const handler = async (event: unknown, context: Context) => { + return { + statusCode: 200, + body: JSON.stringify({ + paymentId: '123', + message: 'Payment created', + }), + }; +}; + +const idempotentHandler = makeIdempotent(handler, { + persistenceStore: cachePersistenceStore, +}); + +export { idempotentHandler, handler }; From feead52075b42bbe77457741fe4eb6c494209145 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 16:29:17 +0600 Subject: [PATCH 07/24] refactor: remove `CacheClient` type --- examples/snippets/idempotency/workingWithLocalCacheRedis.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/snippets/idempotency/workingWithLocalCacheRedis.ts b/examples/snippets/idempotency/workingWithLocalCacheRedis.ts index fd749c818..da2ff0874 100644 --- a/examples/snippets/idempotency/workingWithLocalCacheRedis.ts +++ b/examples/snippets/idempotency/workingWithLocalCacheRedis.ts @@ -1,14 +1,13 @@ import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; -import type { CacheClient } from '@aws-lambda-powertools/idempotency/cache/types'; import { createClient } from '@redis/client'; import type { Context } from 'aws-lambda'; // Initialize the Redis client -const client = (await createClient({ +const client = await createClient({ url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`, username: 'default', -}).connect()) as CacheClient; +}).connect(); const cachePersistenceStore = new CachePersistenceLayer({ client, From 624e9ad0c4428e21b7d5da9396bf18e41cc77a27 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 17:05:08 +0600 Subject: [PATCH 08/24] fix: correct TypeScript syntax in Redis test example --- docs/features/idempotency.md | 2 +- .../snippets/idempotency/workingWithLocalCacheRedis.test.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index abd2db951..0ac0cfab3 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -961,7 +961,7 @@ When testing your Lambda function locally, you can use a local redis instance to === "handler.test.ts" - ```typescript hl_lines="20" + ```typescript hl_lines="19" --8<-- "examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts" ``` diff --git a/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts b/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts index 75e7291eb..24184bfa7 100644 --- a/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts +++ b/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts @@ -1,6 +1,5 @@ import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; -import type { CacheClient } from '@aws-lambda-powertools/idempotency/cache/types'; import { createClient } from '@redis/client'; import type { Context } from 'aws-lambda'; import { describe, expect, it } from 'vitest'; @@ -16,9 +15,9 @@ const context = { } as Context; // Initialize the Redis client with local Redis server -const client = (await createClient({ +const client = await createClient({ url: 'redis://localhost:6379', -}).connect()) as CacheClient; +}).connect(); const mockPersistenceStore = new CachePersistenceLayer({ client, From fdd83cb73f10eca7848dd6628eaf4bab6acfc5d7 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 17:18:19 +0600 Subject: [PATCH 09/24] doc: add race condition diagram for Cache in idempotency documentation --- docs/features/idempotency.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 0ac0cfab3..1f4506da6 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -580,6 +580,30 @@ sequenceDiagram Optional idempotency key +#### Race condition with Cache + +
+```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; +``` +Race condition with Cache +
+ ### Persistence layers #### DynamoDBPersistenceLayer From 9eadf2c8ab465b9a79a0bf0dac06a6e56d31232c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 17:20:15 +0600 Subject: [PATCH 10/24] doc: update reference from Redis to Cache in idempotency documentation --- docs/features/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 1f4506da6..3e2cb55d4 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -80,7 +80,7 @@ To start, you'll need: --- - [Amazon DynamoDB](#dynamodb-table) or [Redis](#redis-database) + [Amazon DynamoDB](#dynamodb-table) or [Cache](#cache-database) * :simple-awslambda:{ .lg .middle } **AWS Lambda function** From fd56d7f63ac84acd2f7d6fa8b43096874de9b1e0 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 17:21:51 +0600 Subject: [PATCH 11/24] doc: remove redundancy in DynamoDB table creation instructions --- docs/features/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 3e2cb55d4..6599848c9 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -94,7 +94,7 @@ Before getting started, you need to create a persistent storage layer where the #### DynamoDB table -As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first. +You'll need to create a table first. **Default table configuration** From 3831cd2aee9dd382d85ecfc6034d6c708d13f1ae Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 17:29:57 +0600 Subject: [PATCH 12/24] doc: simplify instructions for creating a DynamoDB table in idempotency documentation --- docs/features/idempotency.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 6599848c9..05b626090 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -94,11 +94,7 @@ Before getting started, you need to create a persistent storage layer where the #### DynamoDB table -You'll need to create a table first. - -**Default table configuration** - -If you're not [changing the default configuration for the DynamoDB persistence layer](#dynamodbpersistencelayer), this is the expected default configuration: +Unless you're looking to use an [existing table or customize each attribute](#dynamodbpersistencelayer), you only need the following: | Configuration | Default value | Notes | | ------------------ | :------------ | -------------------------------------------------------------------------------------- | From ad200028ad0b798e7330adab60548fb1795c6f4c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 17:50:34 +0600 Subject: [PATCH 13/24] doc: update references to Redis in idempotency documentation --- docs/features/idempotency.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 05b626090..f1ad8bbf6 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -80,7 +80,7 @@ To start, you'll need: --- - [Amazon DynamoDB](#dynamodb-table) or [Cache](#cache-database) + [Amazon DynamoDB](#dynamodb-table) or [Redis](#redis-database) * :simple-awslambda:{ .lg .middle } **AWS Lambda function** @@ -94,7 +94,11 @@ Before getting started, you need to create a persistent storage layer where the #### DynamoDB table -Unless you're looking to use an [existing table or customize each attribute](#dynamodbpersistencelayer), you only need the following: +As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first. + +**Default table configuration** + +If you're not [changing the default configuration for the DynamoDB persistence layer](#dynamodbpersistencelayer), this is the expected default configuration: | Configuration | Default value | Notes | | ------------------ | :------------ | -------------------------------------------------------------------------------------- | @@ -628,7 +632,12 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by #### CachePersistenceLayer -The `CachePersistenceLayer` enables you to use Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer for idempotency state. To use it, initialize `CachePersistenceLayer` with a connected Redis-compatible client. +The `CachePersistenceLayer` enables you to use Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer for idempotency state. You need to bring your own cache client. + +We recommend using [valkey-glide](https://valkey.io/valkey-glide/#__tabbed_2_2){target="_blank"} or [redis-client](https://www.npmjs.com/package/@redis/client){target="_blank"}. But you can use any Redis-compatible client. + +???+ info + Ensure that your cache client is properly configured and connected to your cache instance before using it with `CachePersistenceLayer`. === "Using Valkey Client" ```typescript hl_lines="9-18 21" From fbcec70a3d2016071579cb40fd0974b0733f55e5 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 21:24:46 +0600 Subject: [PATCH 14/24] doc: clarify language in getting started section and update DynamoDB table creation instructions --- docs/features/idempotency.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index f1ad8bbf6..3784b7c94 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -50,7 +50,7 @@ classDiagram ## Getting started -We use Amazon DynamoDB as the default persistence layer in the documentation. If you prefer Cache, you can learn more from [this section](#cache-database). +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-database). ### Installation @@ -94,11 +94,7 @@ Before getting started, you need to create a persistent storage layer where the #### DynamoDB table -As of now, Amazon DynamoDB is the only supported persistent storage layer, so you'll need to create a table first. - -**Default table configuration** - -If you're not [changing the default configuration for the DynamoDB persistence layer](#dynamodbpersistencelayer), this is the expected default configuration: +Unless you're looking to use an [existing table or customize each attribute](#dynamodbpersistencelayer), you only need the following: | Configuration | Default value | Notes | | ------------------ | :------------ | -------------------------------------------------------------------------------------- | From 99d25fa07abf0c4643aa982e58540b51dbc9c7f8 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 21:28:52 +0600 Subject: [PATCH 15/24] doc: update references from cache database to cache service in idempotency documentation --- docs/features/idempotency.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 3784b7c94..da95e89cc 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -50,7 +50,7 @@ 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-database). +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 @@ -134,7 +134,7 @@ Unless you're looking to use an [existing table or customize each attribute](#dy 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 database +#### Cache service We recommend you start with a Redis compatible management services such as [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB for Redis](https://aws.amazon.com/memorydb/){target="_blank"}. From 284c98e6ab0921fb908eaef297601055fd8980bb Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 21:31:37 +0600 Subject: [PATCH 16/24] doc: rephrase recommendation for starting with managed cache services in idempotency documentation --- docs/features/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index da95e89cc..3ea5b558e 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -136,7 +136,7 @@ Unless you're looking to use an [existing table or customize each attribute](#dy #### Cache service -We recommend you start with a Redis compatible management services such as [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB for Redis](https://aws.amazon.com/memorydb/){target="_blank"}. +We recommend starting with a managed cache service, such as [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB for Redis](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. From 4fe6d89e938d4a59265a42495228e786439e99a6 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 21:33:20 +0600 Subject: [PATCH 17/24] doc: update cache service recommendations to include Valkey and Redis OSS --- docs/features/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 3ea5b558e..249bfac75 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -136,7 +136,7 @@ Unless you're looking to use an [existing table or customize each attribute](#dy #### Cache service -We recommend starting with a managed cache service, such as [Amazon ElastiCache for Redis](https://aws.amazon.com/elasticache/redis/){target="_blank"} or [Amazon MemoryDB for Redis](https://aws.amazon.com/memorydb/){target="_blank"}. +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. From fbabd78f185904996fc52359b4a8d07eae51d2a4 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 16 May 2025 23:29:33 +0600 Subject: [PATCH 18/24] doc: improve clarity in CachePersistenceLayer documentation and quick start example --- docs/features/idempotency.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 249bfac75..0cbc89ef2 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -165,7 +165,7 @@ In both services, you'll need to configure [VPC access](https://docs.aws.amazon. 2. Replace the Security Group ID and Subnet ID to match your VPC settings. -Once setup, you can find a quick start example for Cache in [the persistent layers section](#cachepersistencelayer). +Once setup, you can find a quick start example for using a cache in [the persistent layers section](#cachepersistencelayer). ### MakeIdempotent function wrapper @@ -628,12 +628,12 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by #### 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 bring your own cache client. +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"} or [redis-client](https://www.npmjs.com/package/@redis/client){target="_blank"}. But you can use any Redis-compatible 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 - Ensure that your cache client is properly configured and connected to your cache instance before using it with `CachePersistenceLayer`. + Make sure your cache client is configured and connected before using it with `CachePersistenceLayer`. === "Using Valkey Client" ```typescript hl_lines="9-18 21" From bd490c7a8e2befaea402724dd7939f483499fecd Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sat, 17 May 2025 10:37:23 +0600 Subject: [PATCH 19/24] doc: add Valkey and Redis examples for local cache testing in idempotency documentation --- docs/features/idempotency.md | 18 +++++- .../workingWithLocalCacheValkey.test.ts | 56 +++++++++++++++++++ .../workingWithLocalCacheValkey.ts | 36 ++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts create mode 100644 examples/snippets/idempotency/workingWithLocalCacheValkey.ts diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 0cbc89ef2..27cfd8b1d 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -982,15 +982,27 @@ When testing your Lambda function locally, you can use a local DynamoDB instance ### Testing with local Cache -When testing your Lambda function locally, you can use a local redis instance to test the idempotency feature. You can use [Redis OSS](https://redis.io/docs/latest/get-started/){target="_blank"} or [Valkey](https://valkey.io/topics/installation/){target="_blank"} as a local server. +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. -=== "handler.test.ts" +=== "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" ``` -=== "handler.ts" +=== "redisHandler.ts" ```typescript --8<-- "examples/snippets/idempotency/workingWithLocalCacheRedis.ts" diff --git a/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts b/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts new file mode 100644 index 000000000..47f0e5690 --- /dev/null +++ b/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts @@ -0,0 +1,56 @@ +import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; +import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; +import { GlideClient } from '@valkey/valkey-glide'; +import type { Context } from 'aws-lambda'; +import { describe, expect, it } from 'vitest'; +import { handler } from './workingWithLocalDynamoDB.js'; + +const context = { + functionName: 'foo-bar-function', + memoryLimitInMB: '128', + invokedFunctionArn: + 'arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function', + awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e812345678', + getRemainingTimeInMillis: () => 1234, +} as Context; + +// Initialize the Glide client with local Glide server +const client = await GlideClient.createClient({ + addresses: [ + { + host: 'localhost', + port: 6379, + }, + ], + requestTimeout: 5000, +}); + +const mockPersistenceStore = new CachePersistenceLayer({ + client, +}); + +describe('Idempotent handler', () => { + it('returns the same response', async () => { + // Prepare + const idempotentHandler = makeIdempotent(handler, { + persistenceStore: mockPersistenceStore, + }); + + // Act + const response = await idempotentHandler( + { + foo: 'bar', + }, + context + ); + + // Assess + expect(response).toEqual({ + statusCode: 200, + body: JSON.stringify({ + paymentId: '123', + message: 'Payment created', + }), + }); + }); +}); diff --git a/examples/snippets/idempotency/workingWithLocalCacheValkey.ts b/examples/snippets/idempotency/workingWithLocalCacheValkey.ts new file mode 100644 index 000000000..38afbc1fc --- /dev/null +++ b/examples/snippets/idempotency/workingWithLocalCacheValkey.ts @@ -0,0 +1,36 @@ +import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; +import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; +import { GlideClient } from '@valkey/valkey-glide'; +import type { Context } from 'aws-lambda'; + +// 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 cachePersistenceStore = new CachePersistenceLayer({ + client, +}); + +const handler = async (event: unknown, context: Context) => { + return { + statusCode: 200, + body: JSON.stringify({ + paymentId: '123', + message: 'Payment created', + }), + }; +}; + +const idempotentHandler = makeIdempotent(handler, { + persistenceStore: cachePersistenceStore, +}); + +export { idempotentHandler, handler }; From 398b39c757c6a7055fb1584fd18a87f9d0bcab11 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sat, 17 May 2025 10:45:22 +0600 Subject: [PATCH 20/24] doc: update reference from Redis to Cache in getting started section of idempotency documentation --- docs/features/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 27cfd8b1d..36d27b381 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -80,7 +80,7 @@ To start, you'll need: --- - [Amazon DynamoDB](#dynamodb-table) or [Redis](#redis-database) + [Amazon DynamoDB](#dynamodb-table) or [Cache](#cache-service) * :simple-awslambda:{ .lg .middle } **AWS Lambda function** From 592e1a5204858466bc7845159a44b446f0906c44 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sat, 17 May 2025 10:47:18 +0600 Subject: [PATCH 21/24] doc: correct casing for "Redis-compatible" in CachePersistenceLayer recommendations --- docs/features/idempotency.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 36d27b381..3d989c706 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -630,7 +630,7 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by 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. +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`. From 1ae3de4a83e32913e09e8f42b26c210c7078c983 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 19 May 2025 11:10:21 +0200 Subject: [PATCH 22/24] chore: reorg structure --- docs/features/idempotency.md | 123 +++---- .../idempotency/cachePersistenceLayerRedis.ts | 28 +- .../cachePersistenceLayerValkey.ts | 32 +- ...y.ts => customizeCachePersistenceLayer.ts} | 30 +- .../customizeCachePersistenceLayerRedis.ts | 41 --- .../idempotency/templates/cacheCdk.ts | 99 ++++++ ...verlessCloudformation.yml => cacheSam.yml} | 15 +- .../idempotency/templates/tableSam.yaml | 2 +- .../valkeyServerlessCloudformation.yml | 31 -- examples/snippets/package.json | 2 + package-lock.json | 314 +++++++++++++++++- 11 files changed, 502 insertions(+), 215 deletions(-) rename examples/snippets/idempotency/{customizeCachePersistenceLayerValkey.ts => customizeCachePersistenceLayer.ts} (62%) delete mode 100644 examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts create mode 100644 examples/snippets/idempotency/templates/cacheCdk.ts rename examples/snippets/idempotency/templates/{redisServerlessCloudformation.yml => cacheSam.yml} (64%) delete mode 100644 examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 3d989c706..1fb3b4af9 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -50,49 +50,20 @@ 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). +!!! tip + 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. -### Installation - -Install the library in your project +### Amazon DynamoDB ```shell npm i @aws-lambda-powertools/idempotency @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb ``` -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. - -???+ note - 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. - -### IAM Permissions - -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. - -### Required resources - -To start, you'll need: - - - -
-* :octicons-database-16:{ .lg .middle } __Persistent storage__ - - --- - - [Amazon DynamoDB](#dynamodb-table) or [Cache](#cache-service) - -* :simple-awslambda:{ .lg .middle } **AWS Lambda function** - - --- +#### IAM Permissions - With permissions to use your persistent storage +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. -
- -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. - -#### DynamoDB table +#### Table configuration Unless you're looking to use an [existing table or customize each attribute](#dynamodbpersistencelayer), you only need the following: @@ -101,8 +72,7 @@ Unless you're looking to use an [existing table or customize each attribute](#dy | Partition key | `id` | The id of each idempotency record which a combination of `functionName#hashOfPayload`. | | TTL attribute name | `expiration` | This can only be configured after your table is created if you're using AWS Console. | -???+ tip "Tip: You can share a single state table for all functions" - 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. +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. === "AWS Cloud Development Kit (CDK) example" @@ -122,50 +92,51 @@ Unless you're looking to use an [existing table or customize each attribute](#dy --8<-- "examples/snippets/idempotency/templates/tableTerraform.tf" ``` -???+ warning "Warning: Large responses with DynamoDB persistence layer" - 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"}. +##### Limitations - Larger items cannot be written to DynamoDB and will cause exceptions. +* **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. +* **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. +* **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. -???+ info "Info: DynamoDB" - 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, - 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. - 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. - 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 database -#### Cache service +Depending on the persistence layer you want to use, install the library and the corresponding peer dependencies. -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"}. +=== "Valkey" + ```shell + npm i @aws-lambda-powertools/idempotency @valkey/valkey-glide + ``` -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. +=== "Redis OSS" + ```shell + npm i @aws-lambda-powertools/idempotency @redis/client + ``` -##### Cache IaC examples +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"}. -!!! tip inline end "Prefer AWS Console/CLI?" +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. - 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"} +#### Cache configuration -=== "Valkey AWS CloudFormation example" +=== "AWS Cloud Development Kit (CDK) example" - ```yaml hl_lines="5 21" - --8<-- "examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml" + ```typescript title="template.ts" hl_lines="43" + --8<-- "examples/snippets/idempotency/templates/cacheCdk.ts" ``` - 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. + 1. Replace the VPC ID to match your VPC settings. + 2. Replace the Security Group ID to match your VPC settings. + 3. You can use the same template for Redis OSS, just replace the engine to `redis`. -=== "Redis AWS CloudFormation example" +=== "AWS Serverless Application Model (SAM) example" - ```yaml hl_lines="5 21" - --8<-- "examples/snippets/idempotency/templates/redisServerlessCloudformation.yml" + ```yaml hl_lines="6" + --8<-- "examples/snippets/idempotency/templates/cacheSam.yml" ``` - 1. Replace the Security Group ID and Subnet ID to match your VPC settings. + 1. You can use the same template for Redis OSS, just replace the engine to `redis`. 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). + 3. Replace the Security Group ID and Subnet ID to match your VPC settings. ### MakeIdempotent function wrapper @@ -630,42 +601,36 @@ When using DynamoDB as a persistence layer, you can alter the attribute names by 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. +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. ???+ 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" + ```typescript hl_lines="4 7-16 19" + --8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts:5:" ``` === "Using Redis Client" - ```typescript hl_lines="9-12 15" - --8<-- "examples/snippets/idempotency/cachePersistenceLayerRedis.ts" + ```typescript hl_lines="4 7-10 13" + --8<-- "examples/snippets/idempotency/cachePersistenceLayerRedis.ts:5:" ``` -##### 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 | +| **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" - ``` +=== "Customizing CachePersistenceLayer" -=== "Using Redis" - ```typescript hl_lines="16-20" - --8<-- "examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts" + ```typescript hl_lines="20-24" + --8<-- "examples/snippets/idempotency/customizeCachePersistenceLayer.ts:5:" ``` ## Advanced diff --git a/examples/snippets/idempotency/cachePersistenceLayerRedis.ts b/examples/snippets/idempotency/cachePersistenceLayerRedis.ts index ef3a073ca..18785fb82 100644 --- a/examples/snippets/idempotency/cachePersistenceLayerRedis.ts +++ b/examples/snippets/idempotency/cachePersistenceLayerRedis.ts @@ -1,11 +1,13 @@ +declare function processPayment(): Promise<{ + paymentId: string; +}>; + 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', @@ -15,21 +17,15 @@ const persistenceStore = new CachePersistenceLayer({ client, }); -export const handler = middy( - async (_event: Request, _context: Context): Promise => { - try { - // ... create payment +export const handler = middy(async (_event: unknown, _context: Context) => { + const payment = await processPayment(); - return { - paymentId: '1234567890', - message: 'success', - statusCode: 200, - }; - } catch (error) { - throw new Error('Error creating payment'); - } - } -).use( + return { + paymentId: payment?.paymentId, + message: 'success', + statusCode: 200, + }; +}).use( makeHandlerIdempotent({ persistenceStore, }) diff --git a/examples/snippets/idempotency/cachePersistenceLayerValkey.ts b/examples/snippets/idempotency/cachePersistenceLayerValkey.ts index 3e9e1c080..172b8b277 100644 --- a/examples/snippets/idempotency/cachePersistenceLayerValkey.ts +++ b/examples/snippets/idempotency/cachePersistenceLayerValkey.ts @@ -1,41 +1,37 @@ +declare function processPayment(): Promise<{ + paymentId: string; +}>; + 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, + host: String(process.env.CACHE_ENDPOINT), port: Number(process.env.CACHE_PORT), }, ], useTLS: true, - requestTimeout: 5000, + requestTimeout: 2000, }); const persistenceStore = new CachePersistenceLayer({ client, }); -export const handler = middy( - async (_event: Request, _context: Context): Promise => { - try { - // ... create payment +export const handler = middy(async (_event: unknown, _context: Context) => { + const payment = await processPayment(); - return { - paymentId: '1234567890', - message: 'success', - statusCode: 200, - }; - } catch (error) { - throw new Error('Error creating payment'); - } - } -).use( + return { + paymentId: payment?.paymentId, + message: 'success', + statusCode: 200, + }; +}).use( makeHandlerIdempotent({ persistenceStore, }) diff --git a/examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts b/examples/snippets/idempotency/customizeCachePersistenceLayer.ts similarity index 62% rename from examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts rename to examples/snippets/idempotency/customizeCachePersistenceLayer.ts index f166c151f..5b4cf08b5 100644 --- a/examples/snippets/idempotency/customizeCachePersistenceLayerValkey.ts +++ b/examples/snippets/idempotency/customizeCachePersistenceLayer.ts @@ -1,15 +1,17 @@ +declare function processPayment(): Promise<{ + paymentId: string; +}>; + 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, + host: String(process.env.CACHE_ENDPOINT), port: Number(process.env.CACHE_PORT), }, ], @@ -26,21 +28,15 @@ const persistenceStore = new CachePersistenceLayer({ validationKeyAttr: 'validationKey', }); -export const handler = middy( - async (_event: Request, _context: Context): Promise => { - try { - // ... create payment +export const handler = middy(async (_event: unknown, _context: Context) => { + const payment = await processPayment(); - return { - paymentId: '1234567890', - message: 'success', - statusCode: 200, - }; - } catch (error) { - throw new Error('Error creating payment'); - } - } -).use( + return { + paymentId: payment?.paymentId, + message: 'success', + statusCode: 200, + }; +}).use( makeHandlerIdempotent({ persistenceStore, }) diff --git a/examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts b/examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts deleted file mode 100644 index 33a6398dc..000000000 --- a/examples/snippets/idempotency/customizeCachePersistenceLayerRedis.ts +++ /dev/null @@ -1,41 +0,0 @@ -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 => { - try { - // ... create payment - - return { - paymentId: '1234567890', - message: 'success', - statusCode: 200, - }; - } catch (error) { - throw new Error('Error creating payment'); - } - } -).use( - makeHandlerIdempotent({ - persistenceStore, - }) -); diff --git a/examples/snippets/idempotency/templates/cacheCdk.ts b/examples/snippets/idempotency/templates/cacheCdk.ts new file mode 100644 index 000000000..f1a6cbdf5 --- /dev/null +++ b/examples/snippets/idempotency/templates/cacheCdk.ts @@ -0,0 +1,99 @@ +import { Duration, RemovalPolicy, Stack, type StackProps } from 'aws-cdk-lib'; +import { Port, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { CfnServerlessCache } from 'aws-cdk-lib/aws-elasticache'; +import { + Architecture, + Code, + LayerVersion, + Runtime, +} from 'aws-cdk-lib/aws-lambda'; +import { NodejsFunction, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import type { Construct } from 'constructs'; + +export class ValkeyStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = Vpc.fromLookup(this, 'MyVpc', { + vpcId: 'vpc-{your_vpc_id}', // (1)! + }); + + const fnSecurityGroup = new SecurityGroup(this, 'ValkeyFnSecurityGroup', { + vpc, + allowAllOutbound: true, + description: 'Security group for Valkey function', + }); + + const idempotencyCacheSG = SecurityGroup.fromSecurityGroupId( + this, + 'IdempotencyCacheSG', + 'security-{your_sg_id}' // (2)! + ); + idempotencyCacheSG.addIngressRule( + fnSecurityGroup, + Port.tcp(6379), + 'Allow Lambda to connect to serverless cache' + ); + + const serverlessCache = new CfnServerlessCache( + this, + 'MyCfnServerlessCache', + { + engine: 'valkey', // (3)! + majorEngineVersion: '8', + serverlessCacheName: 'idempotency-cache', + subnetIds: [ + vpc.privateSubnets[0].subnetId, + vpc.privateSubnets[1].subnetId, + ], + securityGroupIds: [idempotencyCacheSG.securityGroupId], + } + ); + + const valkeyLayer = new LayerVersion(this, 'ValkeyLayer', { + removalPolicy: RemovalPolicy.DESTROY, + compatibleArchitectures: [Architecture.ARM_64], + compatibleRuntimes: [Runtime.NODEJS_22_X], + code: Code.fromAsset('./lib/layers/valkey-glide'), + }); + + const fnName = 'ValkeyFn'; + const logGroup = new LogGroup(this, 'MyLogGroup', { + logGroupName: `/aws/lambda/${fnName}`, + removalPolicy: RemovalPolicy.DESTROY, + retention: RetentionDays.ONE_DAY, + }); + const fn = new NodejsFunction(this, 'MyFunction', { + functionName: fnName, + logGroup, + runtime: Runtime.NODEJS_22_X, + architecture: Architecture.ARM_64, + memorySize: 512, + timeout: Duration.seconds(30), + entry: './src/idempotency.ts', + handler: 'handler', + layers: [valkeyLayer], + bundling: { + minify: true, + mainFields: ['module', 'main'], + sourceMap: true, + format: OutputFormat.ESM, + externalModules: ['@valkey/valkey-glide'], + metafile: true, + banner: + "import { createRequire } from 'module';const require = createRequire(import.meta.url);", + }, + vpc, + securityGroups: [fnSecurityGroup], + }); + fn.addEnvironment( + 'CACHE_ENDPOINT', + serverlessCache.getAtt('Endpoint.Address').toString() + ); + fn.addEnvironment( + 'CACHE_PORT', + serverlessCache.getAtt('Endpoint.Port').toString() + ); + } +} diff --git a/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml b/examples/snippets/idempotency/templates/cacheSam.yml similarity index 64% rename from examples/snippets/idempotency/templates/redisServerlessCloudformation.yml rename to examples/snippets/idempotency/templates/cacheSam.yml index 1d995eedf..454165190 100644 --- a/examples/snippets/idempotency/templates/redisServerlessCloudformation.yml +++ b/examples/snippets/idempotency/templates/cacheSam.yml @@ -2,12 +2,12 @@ AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Resources: - RedisServerlessIdempotency: + CacheIdempotency: Type: AWS::ElastiCache::ServerlessCache Properties: - Engine: redis - ServerlessCacheName: redis-cache - SecurityGroupIds: # (1)! + Engine: valkey # (1)! + ServerlessCacheName: idempotency-cache + SecurityGroupIds: # (2)! - security-{your_sg_id} SubnetIds: - subnet-{your_subnet_id_1} @@ -18,7 +18,7 @@ Resources: Properties: Runtime: nodejs22.x Handler: index.js - VpcConfig: # (2)! + VpcConfig: # (3)! SecurityGroupIds: - security-{your_sg_id} SubnetIds: @@ -26,6 +26,5 @@ Resources: - subnet-{your_subnet_id_2} Environment: Variables: - POWERTOOLS_SERVICE_NAME: sample - REDIS_HOST: !GetAtt RedisServerlessIdempotency.Endpoint.Address - REDIS_PORT: !GetAtt RedisServerlessIdempotency.Endpoint.Port \ No newline at end of file + CACHE_ENDPOINT: !GetAtt CacheIdempotency.Endpoint.Address + CACHE_PORT: !GetAtt CacheIdempotency.Endpoint.Port \ No newline at end of file diff --git a/examples/snippets/idempotency/templates/tableSam.yaml b/examples/snippets/idempotency/templates/tableSam.yaml index 9de5e226d..38f1a4a06 100644 --- a/examples/snippets/idempotency/templates/tableSam.yaml +++ b/examples/snippets/idempotency/templates/tableSam.yaml @@ -18,7 +18,7 @@ Resources: Type: AWS::Serverless::Function Properties: Runtime: nodejs22.x - Handler: app.py + Handler: index.js Policies: - Statement: - Sid: AllowDynamodbReadWrite diff --git a/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml b/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml deleted file mode 100644 index e1519712f..000000000 --- a/examples/snippets/idempotency/templates/valkeyServerlessCloudformation.yml +++ /dev/null @@ -1,31 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" -Transform: AWS::Serverless-2016-10-31 - -Resources: - ValkeyServerlessIdempotency: - Type: AWS::ElastiCache::ServerlessCache - Properties: - Engine: valkey - ServerlessCacheName: valkey-cache - SecurityGroupIds: # (1)! - - security-{your_sg_id} - SubnetIds: - - subnet-{your_subnet_id_1} - - subnet-{your_subnet_id_2} - - HelloWorldFunction: - Type: AWS::Serverless::Function - Properties: - Runtime: nodejs22.x - Handler: index.js - VpcConfig: # (2)! - SecurityGroupIds: - - security-{your_sg_id} - SubnetIds: - - subnet-{your_subnet_id_1} - - subnet-{your_subnet_id_2} - Environment: - Variables: - POWERTOOLS_SERVICE_NAME: sample - VALKEY_HOST: !GetAtt ValkeyServerlessIdempotency.Endpoint.Address - VALKEY_PORT: !GetAtt ValkeyServerlessIdempotency.Endpoint.Port \ No newline at end of file diff --git a/examples/snippets/package.json b/examples/snippets/package.json index 0ea1ab958..7a925ca0a 100644 --- a/examples/snippets/package.json +++ b/examples/snippets/package.json @@ -39,6 +39,8 @@ "@aws-sdk/client-ssm": "^3.810.0", "@aws-sdk/util-dynamodb": "^3.810.0", "@middy/core": "^4.7.0", + "@redis/client": "^5.0.1", + "@valkey/valkey-glide": "^1.3.4", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", "zod": "^3.24.4" diff --git a/package-lock.json b/package-lock.json index 8d1f122b1..711c0b202 100644 --- a/package-lock.json +++ b/package-lock.json @@ -98,6 +98,8 @@ "@aws-sdk/client-ssm": "^3.810.0", "@aws-sdk/util-dynamodb": "^3.810.0", "@middy/core": "^4.7.0", + "@redis/client": "^5.0.1", + "@valkey/valkey-glide": "^1.3.4", "aws-sdk": "^2.1692.0", "aws-sdk-client-mock": "^4.1.0", "zod": "^3.24.4" @@ -12096,13 +12098,96 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause", + "optional": true, + "peer": true + }, "node_modules/@redis/client": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.0.1.tgz", "integrity": "sha512-k0EJvlMGEyBqUD3orKe0UMZ66fPtfwqPIr+ZSd853sXj2EyhNtPXSx+J6sENXJNgAlEBhvD+57Dwt0qTisKB0A==", + "devOptional": true, "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -13698,6 +13783,184 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" }, + "node_modules/@valkey/valkey-glide": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@valkey/valkey-glide/-/valkey-glide-1.3.4.tgz", + "integrity": "sha512-EXjpGEeTjs2uhJm8ZNkHEK8d3qCQmppmxsv+553S6L9fArZTBvKMmfh7P2H7nxletHx6sxs+fc2UFFw9M5k5SQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "optionalDependencies": { + "@valkey/valkey-glide-darwin-arm64": "1.3.4", + "@valkey/valkey-glide-darwin-x64": "1.3.4", + "@valkey/valkey-glide-linux-arm64": "1.3.4", + "@valkey/valkey-glide-linux-musl-arm64": "1.3.4", + "@valkey/valkey-glide-linux-musl-x64": "1.3.4", + "@valkey/valkey-glide-linux-x64": "1.3.4" + } + }, + "node_modules/@valkey/valkey-glide-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-darwin-arm64/-/valkey-glide-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-Ry8PEWdMtENju/thkqMEDCcf9wJSz5JavpwTJ8+NEaTVeYUSNK7PJ91ZrgxTWyDr51YAdA8e5MjyK2+p2X1HiQ==", + "bundleDependencies": [ + "glide-rs" + ], + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "dependencies": { + "glide-rs": "file:rust-client", + "long": "5", + "protobufjs": "7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@valkey/valkey-glide-darwin-arm64/node_modules/glide-rs": { + "version": "0.1.0", + "inBundle": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@valkey/valkey-glide-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-darwin-x64/-/valkey-glide-darwin-x64-1.3.4.tgz", + "integrity": "sha512-Jl/x1WovhwiP7lWUcA99tw68Usb3GCHZ/ISD/1F23cVErXJ9qB4ZNCFI8Y1QrmwEPSeojKsxSpqylrsInuv1ng==", + "bundleDependencies": [ + "glide-rs" + ], + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "dependencies": { + "glide-rs": "file:rust-client", + "long": "5", + "protobufjs": "7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@valkey/valkey-glide-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-arm64/-/valkey-glide-linux-arm64-1.3.4.tgz", + "integrity": "sha512-ubTZDaEqlwS8NNuIggWt36I1BbtUNE17uHrCaBvUfc+aR1870r4xqvQNyvpQhT/VezfJisSBGKudFdLKeFrIjg==", + "bundleDependencies": [ + "glide-rs" + ], + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "dependencies": { + "glide-rs": "file:rust-client", + "long": "5", + "protobufjs": "7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@valkey/valkey-glide-linux-musl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-musl-arm64/-/valkey-glide-linux-musl-arm64-1.3.4.tgz", + "integrity": "sha512-zNkmKXrT9ebe/XWtevzFleRrXycrzsLtmDjdQJGVUC96gUA3DcczK/gfu2rgvgH84C5s5jKbSM7oeo8f8eX4Vw==", + "bundleDependencies": [ + "glide-rs" + ], + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "dependencies": { + "glide-rs": "file:rust-client", + "long": "5", + "protobufjs": "7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@valkey/valkey-glide-linux-musl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-musl-x64/-/valkey-glide-linux-musl-x64-1.3.4.tgz", + "integrity": "sha512-QSaLTp81V0agKcGXsr3LUF+Vl4nTHrf5eBNV7/2GwbA4Pwed2x8TzORAjv+aS368hWR/9zftbVA1Xjn8TAND3w==", + "bundleDependencies": [ + "glide-rs" + ], + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "dependencies": { + "glide-rs": "file:rust-client", + "long": "5", + "protobufjs": "7" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@valkey/valkey-glide-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@valkey/valkey-glide-linux-x64/-/valkey-glide-linux-x64-1.3.4.tgz", + "integrity": "sha512-j9acGMsr7T1oY377xBsIkY4rjtDqb78/OSH3CFcdntcJZU6u6fvcqGXG5Co/iRDvsm+q9uuYd/H5ITnGx60kng==", + "bundleDependencies": [ + "glide-rs" + ], + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "dependencies": { + "glide-rs": "file:rust-client", + "long": "5", + "protobufjs": "7" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@vitest/coverage-v8": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.3.tgz", @@ -15664,9 +15927,8 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16304,6 +16566,16 @@ "node": ">=4" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devlop": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", @@ -19200,6 +19472,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0", + "optional": true, + "peer": true + }, "node_modules/loupe": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", @@ -22197,6 +22477,32 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/protobufjs": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz", + "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/protocols": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", From eb346415454a54680b679a7714021e0f60592a43 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 19 May 2025 11:11:53 +0200 Subject: [PATCH 23/24] chore: update timeout --- packages/idempotency/src/persistence/CachePersistenceLayer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/idempotency/src/persistence/CachePersistenceLayer.ts b/packages/idempotency/src/persistence/CachePersistenceLayer.ts index b35012bbc..0b48eae3a 100644 --- a/packages/idempotency/src/persistence/CachePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/CachePersistenceLayer.ts @@ -39,11 +39,11 @@ import { IdempotencyRecord } from './IdempotencyRecord.js'; * * const client = await GlideClient.createClient({ * addresses: [{ - * host: process.env.CACHE_ENDPOINT, + * host: String(process.env.CACHE_ENDPOINT), * port: Number(process.env.CACHE_PORT), * }], * useTLS: true, - * requestTimeout: 5000 + * requestTimeout: 2000 * }); * * const persistence = new CachePersistenceLayer({ From 66de3032b76f69d17d7acf783dd3516de0cc14d1 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Mon, 19 May 2025 11:20:15 +0200 Subject: [PATCH 24/24] chore: update testing section --- docs/features/idempotency.md | 20 ++------ .../workingWithLocalCacheRedis.test.ts | 50 ------------------- .../idempotency/workingWithLocalCacheRedis.ts | 30 ----------- .../workingWithLocalCacheValkey.test.ts | 40 ++++----------- .../workingWithLocalCacheValkey.ts | 36 ------------- 5 files changed, 14 insertions(+), 162 deletions(-) delete mode 100644 examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts delete mode 100644 examples/snippets/idempotency/workingWithLocalCacheRedis.ts delete mode 100644 examples/snippets/idempotency/workingWithLocalCacheValkey.ts diff --git a/docs/features/idempotency.md b/docs/features/idempotency.md index 1fb3b4af9..811a40967 100644 --- a/docs/features/idempotency.md +++ b/docs/features/idempotency.md @@ -945,32 +945,20 @@ When testing your Lambda function locally, you can use a local DynamoDB instance --8<-- "examples/snippets/idempotency/workingWithLocalDynamoDB.ts" ``` -### Testing with local Cache +### 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. +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. === "valkeyHandler.test.ts" - ```typescript hl_lines="19-24" + ```typescript hl_lines="5-8" --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" + --8<-- "examples/snippets/idempotency/cachePersistenceLayerValkey.ts:5:" ``` ## Extra resources diff --git a/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts b/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts deleted file mode 100644 index 24184bfa7..000000000 --- a/examples/snippets/idempotency/workingWithLocalCacheRedis.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; -import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; -import { createClient } from '@redis/client'; -import type { Context } from 'aws-lambda'; -import { describe, expect, it } from 'vitest'; -import { handler } from './workingWithLocalDynamoDB.js'; - -const context = { - functionName: 'foo-bar-function', - memoryLimitInMB: '128', - invokedFunctionArn: - 'arn:aws:lambda:eu-west-1:123456789012:function:foo-bar-function', - awsRequestId: 'c6af9ac6-7b61-11e6-9a41-93e812345678', - getRemainingTimeInMillis: () => 1234, -} as Context; - -// Initialize the Redis client with local Redis server -const client = await createClient({ - url: 'redis://localhost:6379', -}).connect(); - -const mockPersistenceStore = new CachePersistenceLayer({ - client, -}); - -describe('Idempotent handler', () => { - it('returns the same response', async () => { - // Prepare - const idempotentHandler = makeIdempotent(handler, { - persistenceStore: mockPersistenceStore, - }); - - // Act - const response = await idempotentHandler( - { - foo: 'bar', - }, - context - ); - - // Assess - expect(response).toEqual({ - statusCode: 200, - body: JSON.stringify({ - paymentId: '123', - message: 'Payment created', - }), - }); - }); -}); diff --git a/examples/snippets/idempotency/workingWithLocalCacheRedis.ts b/examples/snippets/idempotency/workingWithLocalCacheRedis.ts deleted file mode 100644 index da2ff0874..000000000 --- a/examples/snippets/idempotency/workingWithLocalCacheRedis.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; -import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; -import { createClient } from '@redis/client'; -import type { Context } from 'aws-lambda'; - -// Initialize the Redis client -const client = await createClient({ - url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`, - username: 'default', -}).connect(); - -const cachePersistenceStore = new CachePersistenceLayer({ - client, -}); - -const handler = async (event: unknown, context: Context) => { - return { - statusCode: 200, - body: JSON.stringify({ - paymentId: '123', - message: 'Payment created', - }), - }; -}; - -const idempotentHandler = makeIdempotent(handler, { - persistenceStore: cachePersistenceStore, -}); - -export { idempotentHandler, handler }; diff --git a/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts b/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts index 47f0e5690..949886269 100644 --- a/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts +++ b/examples/snippets/idempotency/workingWithLocalCacheValkey.test.ts @@ -1,9 +1,11 @@ -import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; -import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; -import { GlideClient } from '@valkey/valkey-glide'; import type { Context } from 'aws-lambda'; -import { describe, expect, it } from 'vitest'; -import { handler } from './workingWithLocalDynamoDB.js'; +import { describe, expect, it, vi } from 'vitest'; +import { handler } from './cachePersistenceLayerValkey.js'; + +vi.hoisted(() => { + process.env.CACHE_ENDPOINT = 'localhost'; + process.env.CACHE_PORT = '6379'; +}); const context = { functionName: 'foo-bar-function', @@ -14,30 +16,10 @@ const context = { getRemainingTimeInMillis: () => 1234, } as Context; -// Initialize the Glide client with local Glide server -const client = await GlideClient.createClient({ - addresses: [ - { - host: 'localhost', - port: 6379, - }, - ], - requestTimeout: 5000, -}); - -const mockPersistenceStore = new CachePersistenceLayer({ - client, -}); - describe('Idempotent handler', () => { it('returns the same response', async () => { - // Prepare - const idempotentHandler = makeIdempotent(handler, { - persistenceStore: mockPersistenceStore, - }); - // Act - const response = await idempotentHandler( + const response = await handler( { foo: 'bar', }, @@ -46,11 +28,9 @@ describe('Idempotent handler', () => { // Assess expect(response).toEqual({ + paymentId: expect.any(String), + message: 'success', statusCode: 200, - body: JSON.stringify({ - paymentId: '123', - message: 'Payment created', - }), }); }); }); diff --git a/examples/snippets/idempotency/workingWithLocalCacheValkey.ts b/examples/snippets/idempotency/workingWithLocalCacheValkey.ts deleted file mode 100644 index 38afbc1fc..000000000 --- a/examples/snippets/idempotency/workingWithLocalCacheValkey.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { makeIdempotent } from '@aws-lambda-powertools/idempotency'; -import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache'; -import { GlideClient } from '@valkey/valkey-glide'; -import type { Context } from 'aws-lambda'; - -// 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 cachePersistenceStore = new CachePersistenceLayer({ - client, -}); - -const handler = async (event: unknown, context: Context) => { - return { - statusCode: 200, - body: JSON.stringify({ - paymentId: '123', - message: 'Payment created', - }), - }; -}; - -const idempotentHandler = makeIdempotent(handler, { - persistenceStore: cachePersistenceStore, -}); - -export { idempotentHandler, handler };