Skip to content

Commit 49d3e96

Browse files
refactor(idempotency): replace Redis name with Cache and add valkey-glide support (#6685)
* Adding cache name * Replacing Redis with Cache * More changes
1 parent 56a70cf commit 49d3e96

13 files changed

+292
-158
lines changed

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ target:
77
dev:
88
pip install --upgrade pip pre-commit poetry
99
@$(MAKE) dev-version-plugin
10-
poetry install --extras "all redis datamasking"
10+
poetry install --extras "all redis datamasking valkey"
1111
pre-commit install
1212

1313
dev-quality-code:
1414
pip install --upgrade pip pre-commit poetry
1515
@$(MAKE) dev-version-plugin
16-
poetry install --extras "all redis datamasking"
16+
poetry install --extras "all redis datamasking valkey"
1717
pre-commit install
1818

1919
dev-gitpod:
2020
pip install --upgrade pip poetry
21-
poetry install --extras "all redis datamasking"
21+
poetry install --extras "all redis datamasking valkey"
2222
pre-commit install
2323

2424
format-check:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from aws_lambda_powertools.utilities.idempotency.persistence.redis import (
2+
CacheClientProtocol,
3+
CacheConnection,
4+
CachePersistenceLayer,
5+
)
6+
7+
__all__ = [
8+
"CacheClientProtocol",
9+
"CachePersistenceLayer",
10+
"CacheConnection",
11+
]

aws_lambda_powertools/utilities/idempotency/persistence/redis.py

Lines changed: 67 additions & 60 deletions
Large diffs are not rendered by default.

docs/utilities/idempotency.md

Lines changed: 38 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The idempotency utility allows you to retry operations within a time window with
1212
* Produces the previous successful result when a function is called repeatedly with the same idempotency key
1313
* Choose your idempotency key from one or more fields, or entire payload
1414
* Safeguard concurrent requests, timeouts, missing idempotency keys, and payload tampering
15-
* Support for Amazon DynamoDB, Redis, bring your own persistence layer, and in-memory caching
15+
* Support for Amazon DynamoDB, Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer
1616

1717
## Terminology
1818

@@ -82,7 +82,7 @@ To start, you'll need:
8282

8383
---
8484

85-
[Amazon DynamoDB](#dynamodb-table) or [Redis](#redis-database)
85+
[Amazon DynamoDB](#dynamodb-table) or [Valkey/Redis OSS/Redis compatible](#cache-database)
8686

8787
* :simple-awslambda:{ .lg .middle } **AWS Lambda function**
8888

@@ -139,13 +139,13 @@ You **can** use a single DynamoDB table for all functions annotated with Idempot
139139

140140
* **Old boto3 versions can increase costs**. For cost optimization, we use a conditional `PutItem` to always lock a new idempotency record. If locking fails, it means we already have an idempotency record saving us an additional `GetItem` call. However, this is only supported in boto3 `1.26.194` and higher _([June 30th 2023](https://aws.amazon.com/about-aws/whats-new/2023/06/amazon-dynamodb-cost-failed-conditional-writes/){target="_blank"})_.
141141

142-
#### Redis database
142+
#### Cache database
143143

144-
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"}.
144+
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"}.
145145

146146
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.
147147

148-
##### Redis IaC examples
148+
##### Cache configuration
149149

150150
=== "AWS CloudFormation example"
151151

@@ -160,7 +160,7 @@ In both services, you'll need to configure [VPC access](https://docs.aws.amazon.
160160
1. Replace the Security Group ID and Subnet ID to match your VPC settings.
161161
2. Replace the Security Group ID and Subnet ID to match your VPC settings.
162162

163-
Once setup, you can find a quick start and advanced examples for Redis in [the persistent layers section](#redispersistencelayer).
163+
Once setup, you can find a quick start and advanced examples for Cache in [the persistent layers section](#cachepersistencelayer).
164164

165165
<!-- markdownlint-enable MD013 -->
166166

@@ -464,17 +464,22 @@ You can customize the attribute names during initialization:
464464
| **sort_key_attr** | | | Sort key of the table (if table is configured with a sort key). |
465465
| **static_pk_value** | | `idempotency#{LAMBDA_FUNCTION_NAME}` | Static value to use as the partition key. Only used when **sort_key_attr** is set. |
466466

467-
#### RedisPersistenceLayer
467+
#### CachePersistenceLayer
468468

469-
!!! info "We recommend Redis version 7 or higher for optimal performance."
469+
The `CachePersistenceLayer` enables you to use Valkey, Redis OSS, or any Redis-compatible cache as the persistence layer for idempotency state.
470470

471-
For simple setups, initialize `RedisCachePersistenceLayer` with your Redis endpoint and port to connect.
471+
We recommend using [`valkey-glide`](https://pypi.org/project/valkey-glide/){target="_blank"} for Valkey or [`redis`](https://pypi.org/project/redis/){target="_blank"} for Redis. However, any Redis OSS-compatible client should work.
472472

473-
For security, we enforce SSL connections by default; to disable it, set `ssl=False`.
473+
For simple setups, initialize `CachePersistenceLayer` with your Cache endpoint and port to connect. Note that for security, we enforce SSL connections by default; to disable it, set `ssl=False`.
474474

475-
=== "Redis quick start"
476-
```python title="getting_started_with_idempotency_redis_config.py" hl_lines="8-10 14 27"
477-
--8<-- "examples/idempotency/src/getting_started_with_idempotency_redis_config.py"
475+
=== "Cache quick start"
476+
```python title="getting_started_with_idempotency_cache_config.py" hl_lines="8-10 14 27"
477+
--8<-- "examples/idempotency/src/getting_started_with_idempotency_cache_config.py"
478+
```
479+
480+
=== "Using an existing Valkey Glide client"
481+
```python title="getting_started_with_idempotency_valkey_client.py" hl_lines="5 10-12 16-22 24 37"
482+
--8<-- "examples/idempotency/src/getting_started_with_idempotency_valkey_client.py"
478483
```
479484

480485
=== "Using an existing Redis client"
@@ -488,23 +493,23 @@ For security, we enforce SSL connections by default; to disable it, set `ssl=Fal
488493
--8<-- "examples/idempotency/src/getting_started_with_idempotency_payload.json"
489494
```
490495

491-
##### Redis SSL connections
496+
##### Cache SSL connections
492497

493498
We recommend using AWS Secrets Manager to store and rotate certificates safely, and the [Parameters feature](./parameters.md){target="_blank"} to fetch and cache optimally.
494499

495-
For advanced configurations, we recommend using an existing Redis client for optimal compatibility like SSL certificates and timeout.
500+
For advanced configurations, we recommend using an existing Valkey client for optimal compatibility like SSL certificates and timeout.
496501

497502
=== "Advanced configuration using AWS Secrets"
498-
```python title="using_redis_client_with_aws_secrets.py" hl_lines="9-11 13 15 25"
499-
--8<-- "examples/idempotency/src/using_redis_client_with_aws_secrets.py"
503+
```python title="using_cache_client_with_aws_secrets.py" hl_lines="5 9-11 13 15 18 19 23"
504+
--8<-- "examples/idempotency/src/using_cache_client_with_aws_secrets.py"
500505
```
501506

502507
1. JSON stored:
503508
```json
504509
{
505-
"REDIS_ENDPOINT": "127.0.0.1",
506-
"REDIS_PORT": "6379",
507-
"REDIS_PASSWORD": "redis-secret"
510+
"CACHE_HOST": "127.0.0.1",
511+
"CACHE_PORT": "6379",
512+
"CACHE_PASSWORD": "cache-secret"
508513
}
509514
```
510515

@@ -516,16 +521,16 @@ For advanced configurations, we recommend using an existing Redis client for opt
516521
1. JSON stored:
517522
```json
518523
{
519-
"REDIS_ENDPOINT": "127.0.0.1",
520-
"REDIS_PORT": "6379",
521-
"REDIS_PASSWORD": "redis-secret"
524+
"CACHE_HOST": "127.0.0.1",
525+
"CACHE_PORT": "6379",
526+
"CACHE_PASSWORD": "cache-secret"
522527
}
523528
```
524-
2. redis_user.crt file stored in the "certs" directory of your Lambda function
525-
3. redis_user_private.key file stored in the "certs" directory of your Lambda function
526-
4. redis_ca.pem file stored in the "certs" directory of your Lambda function
529+
2. cache_user.crt file stored in the "certs" directory of your Lambda function
530+
3. cache_user_private.key file stored in the "certs" directory of your Lambda function
531+
4. cache_ca.pem file stored in the "certs" directory of your Lambda function
527532

528-
##### Redis attributes
533+
##### Cache attributes
529534

530535
You can customize the attribute names during initialization:
531536

@@ -811,28 +816,28 @@ sequenceDiagram
811816
<i>Optional idempotency key</i>
812817
</center>
813818

814-
#### Race condition with Redis
819+
#### Race condition with Cache
815820

816821
<center>
817822
```mermaid
818823
graph TD;
819-
A(Existing orphan record in redis)-->A1;
824+
A(Existing orphan record in cache)-->A1;
820825
A1[Two Lambda invoke at same time]-->B1[Lambda handler1];
821-
B1-->B2[Fetch from Redis];
826+
B1-->B2[Fetch from Cache];
822827
B2-->B3[Handler1 got orphan record];
823828
B3-->B4[Handler1 acquired lock];
824829
B4-->B5[Handler1 overwrite orphan record]
825830
B5-->B6[Handler1 continue to execution];
826831
A1-->C1[Lambda handler2];
827-
C1-->C2[Fetch from Redis];
832+
C1-->C2[Fetch from Cache];
828833
C2-->C3[Handler2 got orphan record];
829834
C3-->C4[Handler2 failed to acquire lock];
830-
C4-->C5[Handler2 wait and fetch from Redis];
835+
C4-->C5[Handler2 wait and fetch from Cache];
831836
C5-->C6[Handler2 return without executing];
832837
B6-->D(Lambda handler executed only once);
833838
C6-->D;
834839
```
835-
<i>Race condition with Redis</i>
840+
<i>Race condition with Cache</i>
836841
</center>
837842

838843
## Advanced

examples/idempotency/src/getting_started_with_idempotency_redis_config.py renamed to examples/idempotency/src/getting_started_with_idempotency_cache_config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
from aws_lambda_powertools.utilities.idempotency import (
66
idempotent,
77
)
8-
from aws_lambda_powertools.utilities.idempotency.persistence.redis import (
9-
RedisCachePersistenceLayer,
8+
from aws_lambda_powertools.utilities.idempotency.persistence.cache import (
9+
CachePersistenceLayer,
1010
)
1111
from aws_lambda_powertools.utilities.typing import LambdaContext
1212

13-
redis_endpoint = os.getenv("REDIS_CLUSTER_ENDPOINT", "localhost")
14-
persistence_layer = RedisCachePersistenceLayer(host=redis_endpoint, port=6379)
13+
redis_endpoint = os.getenv("CACHE_CLUSTER_ENDPOINT", "localhost")
14+
persistence_layer = CachePersistenceLayer(host=redis_endpoint, port=6379)
1515

1616

1717
@dataclass
@@ -34,7 +34,7 @@ def lambda_handler(event: dict, context: LambdaContext):
3434
"statusCode": 200,
3535
}
3636
except Exception as exc:
37-
raise PaymentError(f"Error creating payment {str(exc)}")
37+
raise PaymentError(f"Error creating payment {str(exc)}") from exc
3838

3939

4040
def create_subscription_payment(event: dict) -> Payment:

examples/idempotency/src/getting_started_with_idempotency_redis_client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,21 @@
77
from aws_lambda_powertools.utilities.idempotency import (
88
idempotent,
99
)
10-
from aws_lambda_powertools.utilities.idempotency.persistence.redis import (
11-
RedisCachePersistenceLayer,
10+
from aws_lambda_powertools.utilities.idempotency.persistence.cache import (
11+
CachePersistenceLayer,
1212
)
1313
from aws_lambda_powertools.utilities.typing import LambdaContext
1414

15-
redis_endpoint = os.getenv("REDIS_CLUSTER_ENDPOINT", "localhost")
15+
cache_endpoint = os.getenv("CACHE_CLUSTER_ENDPOINT", "localhost")
1616
client = Redis(
17-
host=redis_endpoint,
17+
host=cache_endpoint,
1818
port=6379,
1919
socket_connect_timeout=5,
2020
socket_timeout=5,
2121
max_connections=1000,
2222
)
2323

24-
persistence_layer = RedisCachePersistenceLayer(client=client)
24+
persistence_layer = CachePersistenceLayer(client=client)
2525

2626

2727
@dataclass
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import os
2+
from dataclasses import dataclass, field
3+
from uuid import uuid4
4+
5+
from glide import GlideClient, GlideClientConfiguration, NodeAddress
6+
7+
from aws_lambda_powertools.utilities.idempotency import (
8+
idempotent,
9+
)
10+
from aws_lambda_powertools.utilities.idempotency.persistence.cache import (
11+
CachePersistenceLayer,
12+
)
13+
from aws_lambda_powertools.utilities.typing import LambdaContext
14+
15+
cache_endpoint = os.getenv("CACHE_CLUSTER_ENDPOINT", "localhost")
16+
client_config = GlideClientConfiguration(
17+
addresses=[
18+
NodeAddress(
19+
host="localhost",
20+
port=6379,
21+
),
22+
],
23+
)
24+
client = GlideClient.create(config=client_config)
25+
26+
persistence_layer = CachePersistenceLayer(client=client) # type: ignore[arg-type]
27+
28+
29+
@dataclass
30+
class Payment:
31+
user_id: str
32+
product_id: str
33+
payment_id: str = field(default_factory=lambda: f"{uuid4()}")
34+
35+
36+
class PaymentError(Exception): ...
37+
38+
39+
@idempotent(persistence_store=persistence_layer)
40+
def lambda_handler(event: dict, context: LambdaContext):
41+
try:
42+
payment: Payment = create_subscription_payment(event)
43+
return {
44+
"payment_id": payment.payment_id,
45+
"message": "success",
46+
"statusCode": 200,
47+
}
48+
except Exception as exc:
49+
raise PaymentError(f"Error creating payment {str(exc)}")
50+
51+
52+
def create_subscription_payment(event: dict) -> Payment:
53+
return Payment(**event)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
from typing import Any
4+
5+
from glide import BackoffStrategy, GlideClient, GlideClientConfiguration, NodeAddress, ServerCredentials
6+
7+
from aws_lambda_powertools.utilities import parameters
8+
from aws_lambda_powertools.utilities.idempotency import IdempotencyConfig, idempotent
9+
from aws_lambda_powertools.utilities.idempotency.persistence.cache import (
10+
CachePersistenceLayer,
11+
)
12+
13+
cache_values: dict[str, Any] = parameters.get_secret("cache_info", transform="json") # (1)!
14+
15+
client_config = GlideClientConfiguration(
16+
addresses=[
17+
NodeAddress(
18+
host=cache_values.get("CACHE_HOST", "localhost"),
19+
port=cache_values.get("CACHE_PORT", 6379),
20+
),
21+
],
22+
credentials=ServerCredentials(
23+
password=cache_values.get("CACHE_PASSWORD", ""),
24+
),
25+
request_timeout=10,
26+
use_tls=True,
27+
reconnect_strategy=BackoffStrategy(num_of_retries=10, factor=2, exponent_base=1),
28+
)
29+
valkey_client = GlideClient.create(config=client_config)
30+
31+
persistence_layer = CachePersistenceLayer(client=valkey_client) # type: ignore[arg-type]
32+
config = IdempotencyConfig(
33+
expires_after_seconds=2 * 60, # 2 minutes
34+
)
35+
36+
37+
@idempotent(config=config, persistence_store=persistence_layer)
38+
def lambda_handler(event, context):
39+
return {"message": "Hello"}

examples/idempotency/src/using_redis_client_with_aws_secrets.py

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

0 commit comments

Comments
 (0)