Skip to content

feat(idempotency): support for Redis as idempotency backend #3896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
3016a84
feat: add @redis/client as a peer dependency in idempotency package
arnabrahman Apr 20, 2025
2110332
chore: mark @redis/client as an optional dependency
arnabrahman Apr 20, 2025
b0ade6d
chore: mark @redis/client and related dependencies as optional
arnabrahman Apr 20, 2025
b55374d
feat: add Redis persistence layer with connection configuration and p…
arnabrahman Apr 27, 2025
5e7e0f2
fix: update RedisClientProtocol return type for set method
arnabrahman Apr 27, 2025
b63fbb7
feat: add persistence layer error classes for connection and consiste…
arnabrahman Apr 27, 2025
c04db42
feat: implement Redis persistence layer with idempotency record manag…
arnabrahman Apr 27, 2025
d687674
test: unit tests for `RedisConnection`
arnabrahman May 3, 2025
ea55783
test: dummy test class for `RedisPersistenceLayer`
arnabrahman May 3, 2025
499cc8d
fix: make Redis client optional in `RedisPersistenceOptions` interface
arnabrahman May 3, 2025
271617c
refactor: update Redis client connection initialization and add clien…
arnabrahman May 3, 2025
46e25a2
fix: ensure Redis connection is only initialized if the default clien…
arnabrahman May 3, 2025
26870f4
test: add unit tests for `init` method of `RedisPersistenceLayer`
arnabrahman May 3, 2025
479f383
test: ` _putRecord` unit tests
arnabrahman May 4, 2025
f702293
test: payload hash test case for `putRecord`
arnabrahman May 4, 2025
955b273
test: add unit test for `_putRecord` method to verify record insertio…
arnabrahman May 4, 2025
e45492d
fix: correct expiry time calculation in `_getExpirySeconds` method
arnabrahman May 4, 2025
fabfb19
test: `_putRecord` function unit test
arnabrahman May 4, 2025
91afcdf
test: refactor `_putRecord` tests to support both default and user-pr…
arnabrahman May 4, 2025
92a186b
refactor: simplify test scenarios for `_putRecord` method in RedisPer…
arnabrahman May 4, 2025
952c451
test: refactor `_putRecord` tests to initialize layer in beforeEach a…
arnabrahman May 4, 2025
d6d421e
test: add unit tests for `_getRecord` method in RedisPersistenceLayer
arnabrahman May 4, 2025
c7e45c6
test: add unit tests for `_updateRecord` method in RedisPersistenceLayer
arnabrahman May 4, 2025
1be3cd4
fix: correct expiry timestamp units in IdempotencyRecord and related …
arnabrahman May 4, 2025
dda4cf1
refactor: rename private methods for consistency in RedisPersistenceL…
arnabrahman May 4, 2025
93706e5
docs: update class documentation for `RedisPersistenceLayer` with usa…
arnabrahman May 4, 2025
3c6e602
Merge branch 'main' into 3183-idempotency-redis
arnabrahman May 4, 2025
1ca565d
refactor: remove redundant JSON serialization options from `RedisPers…
arnabrahman May 4, 2025
1740ee0
feat: upgrade @redis/client
arnabrahman May 5, 2025
a73bea2
fix: correct SSL option handling in `RedisConnection`
arnabrahman May 5, 2025
5cf6c04
test: add case for standalone client without `tls` when `ssl` is false
arnabrahman May 5, 2025
4efa8de
fix: update Redis client deletion method to accept an array of keys
arnabrahman May 5, 2025
9d007df
fix: correct expiry timestamp documentation in IdempotencyRecordOptions
arnabrahman May 5, 2025
39167e7
fix: throw `IdempotencyUnknownError` for invalid record status in Red…
arnabrahman May 6, 2025
22e5926
fix: improve logging and documentation clarity in `RedisPersistenceLa…
arnabrahman May 6, 2025
32ef8fb
fix: cast Redis client to RedisClientProtocol in usage example
arnabrahman May 6, 2025
b8e8f83
fix: update documentation for RedisConnection class to clarify its pu…
arnabrahman May 6, 2025
e8c1ef2
Merge branch 'main' into 3183-idempotency-redis
arnabrahman May 6, 2025
dd57e13
Merge branch 'main' into 3183-idempotency-redis
dreamorosi May 6, 2025
499635c
Merge branch 'main' into 3183-idempotency-redis
dreamorosi May 6, 2025
2215c1f
refactor: rename `RedisClientProtocol` to `RedisCompatibleClient` for…
arnabrahman May 7, 2025
489bcfc
refactor: remove `RedisConnectionConfig` interface and enforce client…
arnabrahman May 7, 2025
08286e1
refactor: remove `RedisConnection` class and related connection error…
arnabrahman May 7, 2025
d740188
refactor: remove `RedisConnection` references from `RedisPersistenceL…
arnabrahman May 7, 2025
8b73fda
refactor: remove `console.debug` statements from `RedisPersistenceLay…
arnabrahman May 7, 2025
3b9d8a2
fix: add `@valkey/valkey-glide` in dependencies and mark as optional
arnabrahman May 7, 2025
8c792ad
refactor: remove outdated example usage of `RedisPersistenceLayer` fr…
arnabrahman May 7, 2025
d0af3a1
refactor: update documentation for `RedisPersistenceLayer` and `Redis…
arnabrahman May 7, 2025
70f3beb
refactor: remove outdated reference to Redis client documentation fro…
arnabrahman May 7, 2025
78d3aba
refactor: introduce `BasePersistenceOptions` interface for `Dynamodb`…
arnabrahman May 7, 2025
b709f33
refactor: update documentation for `RedisPersistenceOptions` to clari…
arnabrahman May 7, 2025
a509f79
refactor: rename `BasePersistenceOptions` to `BasePersistenceAttribut…
arnabrahman May 7, 2025
6e91573
refactor: introduce DEFAULT_PERSISTENCE_LAYER_ATTRIBUTES for consiste…
arnabrahman May 7, 2025
063edff
refactor: rename `DEFAULT_PERSISTENCE_LAYER_ATTRIBUTES` to `PERSISTEN…
arnabrahman May 7, 2025
42b3e9e
refactor: remove unused `DynamoDBClientConfig` import and associated …
arnabrahman May 7, 2025
ce5773c
Merge branch 'main' into 3183-idempotency-redis
dreamorosi May 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 25 additions & 1 deletion packages/idempotency/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@
"import": "./lib/esm/types/DynamoDBPersistence.js",
"require": "./lib/cjs/types/DynamoDBPersistence.js"
},
"./redis": {
"import": "./lib/esm/persistence/RedisPersistenceLayer.js",
"require": "./lib/cjs/persistence/RedisPersistenceLayer.js"
},
"./redis/types": {
"import": "./lib/esm/types/RedisPersistence.js",
"require": "./lib/cjs/types/RedisPersistence.js"
},
"./middleware": {
"import": "./lib/esm/middleware/makeHandlerIdempotent.js",
"require": "./lib/cjs/middleware/makeHandlerIdempotent.js"
Expand All @@ -75,6 +83,14 @@
"lib/cjs/types/DynamoDBPersistence.d.ts",
"lib/esm/types/DynamoDBPersistence.d.ts"
],
"redis": [
"lib/cjs/persistence/RedisPersistenceLayer.d.ts",
"lib/esm/persistence/RedisPersistenceLayer.d.ts"
],
"redis/types": [
"lib/cjs/types/RedisPersistence.d.ts",
"lib/esm/types/RedisPersistence.d.ts"
],
"middleware": [
"lib/cjs/middleware/makeHandlerIdempotent.d.ts",
"lib/esm/middleware/makeHandlerIdempotent.d.ts"
Expand Down Expand Up @@ -104,7 +120,9 @@
"peerDependencies": {
"@aws-sdk/client-dynamodb": ">=3.x",
"@aws-sdk/lib-dynamodb": ">=3.x",
"@middy/core": "4.x || 5.x || 6.x"
"@middy/core": "4.x || 5.x || 6.x",
"@redis/client": "^5.0.1",
"@valkey/valkey-glide": "^1.3.4"
},
"peerDependenciesMeta": {
"@aws-sdk/client-dynamodb": {
Expand All @@ -115,6 +133,12 @@
},
"@middy/core": {
"optional": true
},
"@redis/client": {
"optional": true
},
"@valkey/valkey-glide": {
"optional": true
}
},
"keywords": [
Expand Down
21 changes: 20 additions & 1 deletion packages/idempotency/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { BasePersistenceAttributes } from './types/BasePersistenceLayer.js';
/**
* Number of times to retry a request in case of `IdempotencyInconsistentStateError`
*
Expand All @@ -20,4 +21,22 @@ const IdempotencyRecordStatus = {
EXPIRED: 'EXPIRED',
} as const;

export { IdempotencyRecordStatus, MAX_RETRIES };
/**
* Base persistence attribute key names for persistence layers
*/
const PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS: Record<
keyof Required<BasePersistenceAttributes>,
string
> = {
statusAttr: 'status',
expiryAttr: 'expiration',
inProgressExpiryAttr: 'in_progress_expiration',
dataAttr: 'data',
validationKeyAttr: 'validation',
} as const;

export {
IdempotencyRecordStatus,
MAX_RETRIES,
PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS,
};
11 changes: 11 additions & 0 deletions packages/idempotency/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,16 @@ class IdempotencyKeyError extends IdempotencyUnknownError {
}
}

/**
* Error with the persistence layer's consistency, needs to be removed
*/
class IdempotencyPersistenceConsistencyError extends IdempotencyUnknownError {
public constructor(message: string, options?: ErrorOptions) {
super(message, options);
this.name = 'IdempotencyPersistenceConsistencyError';
}
}

export {
IdempotencyUnknownError,
IdempotencyItemAlreadyExistsError,
Expand All @@ -122,5 +132,6 @@ export {
IdempotencyValidationError,
IdempotencyInconsistentStateError,
IdempotencyPersistenceLayerError,
IdempotencyPersistenceConsistencyError,
IdempotencyKeyError,
};
5 changes: 4 additions & 1 deletion packages/idempotency/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ export {
export { IdempotencyConfig } from './IdempotencyConfig.js';
export { makeIdempotent } from './makeIdempotent.js';
export { idempotent } from './idempotencyDecorator.js';
export { IdempotencyRecordStatus } from './constants.js';
export {
IdempotencyRecordStatus,
PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS,
} from './constants.js';
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
// envVarsService is always initialized in the constructor
private envVarsService!: EnvironmentVariablesService;
private eventKeyJmesPath?: string;
private expiresAfterSeconds: number = 60 * 60; // 1 hour default
protected expiresAfterSeconds: number = 60 * 60; // 1 hour default
private hashFunction = 'md5';
private payloadValidationEnabled = false;
private throwOnNoIdempotencyKey = false;
Expand Down
23 changes: 15 additions & 8 deletions packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
ConditionalCheckFailedException,
DeleteItemCommand,
DynamoDBClient,
type DynamoDBClientConfig,
GetItemCommand,
PutItemCommand,
UpdateItemCommand,
} from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { IdempotencyRecordStatus } from '../constants.js';
import {
IdempotencyRecordStatus,
PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS,
} from '../constants.js';
import {
IdempotencyItemAlreadyExistsError,
IdempotencyItemNotFoundError,
Expand Down Expand Up @@ -49,7 +51,6 @@ import { IdempotencyRecord } from './IdempotencyRecord.js';
*/
class DynamoDBPersistenceLayer extends BasePersistenceLayer {
private client: DynamoDBClient;
private clientConfig: DynamoDBClientConfig = {};
private dataAttr: string;
private expiryAttr: string;
private inProgressExpiryAttr: string;
Expand All @@ -65,12 +66,18 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {

this.tableName = config.tableName;
this.keyAttr = config.keyAttr ?? 'id';
this.statusAttr = config.statusAttr ?? 'status';
this.expiryAttr = config.expiryAttr ?? 'expiration';
this.statusAttr =
config.statusAttr ?? PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS.statusAttr;
this.expiryAttr =
config.expiryAttr ?? PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS.expiryAttr;
this.inProgressExpiryAttr =
config.inProgressExpiryAttr ?? 'in_progress_expiration';
this.dataAttr = config.dataAttr ?? 'data';
this.validationKeyAttr = config.validationKeyAttr ?? 'validation';
config.inProgressExpiryAttr ??
PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS.inProgressExpiryAttr;
this.dataAttr =
config.dataAttr ?? PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS.dataAttr;
this.validationKeyAttr =
config.validationKeyAttr ??
PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS.validationKeyAttr;
if (config.sortKeyAttr === this.keyAttr) {
throw new Error(
`keyAttr [${this.keyAttr}] and sortKeyAttr [${config.sortKeyAttr}] cannot be the same!`
Expand Down
2 changes: 1 addition & 1 deletion packages/idempotency/src/persistence/IdempotencyRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { DynamoDBPersistenceLayer } from './DynamoDBPersistenceLayer.js';
*/
class IdempotencyRecord {
/**
* The expiry timestamp of the record in milliseconds UTC.
* The expiry timestamp of the record in seconds UTC.
*/
public expiryTimestamp?: number;
/**
Expand Down
Loading
Loading