1
- import type { JSONObject } from '@aws-lambda-powertools/commons/types' ;
2
1
import {
3
2
IdempotencyRecordStatus ,
4
3
PERSISTENCE_ATTRIBUTE_KEY_MAPPINGS ,
@@ -9,54 +8,81 @@ import {
9
8
IdempotencyPersistenceConsistencyError ,
10
9
IdempotencyUnknownError ,
11
10
} from '../errors.js' ;
12
- import type { IdempotencyRecordStatusValue } from '../types/IdempotencyRecord.js' ;
13
11
import type {
14
- RedisCompatibleClient ,
15
- RedisPersistenceOptions ,
16
- } from '../types/RedisPersistence.js' ;
12
+ CacheClient ,
13
+ CachePersistenceOptions ,
14
+ } from '../types/CachePersistence.js' ;
15
+ import type { IdempotencyRecordStatusValue } from '../types/IdempotencyRecord.js' ;
17
16
import { BasePersistenceLayer } from './BasePersistenceLayer.js' ;
18
17
import { IdempotencyRecord } from './IdempotencyRecord.js' ;
19
18
20
19
/**
21
- * Redis persistence layer for idempotency records.
20
+ * Valey- and Redis OOS-compatible persistence layer for idempotency records.
22
21
*
23
- * This class uses Redis to write and read idempotency records. It supports any Redis client that
24
- * implements the RedisCompatibleClient interface.
22
+ * This class uses a cache client to write and read idempotency records. It supports any client that
23
+ * implements the { @link CacheClient | `CacheClient`} interface.
25
24
*
26
25
* There are various options to configure the persistence layer, such as attribute names for storing
27
- * status, expiry, data, and validation keys in Redis .
26
+ * status, expiry, data, and validation keys in the cache .
28
27
*
29
- * You must provide your own connected Redis client instance by passing it through the `client` option.
28
+ * You must provide your own connected client instance by passing it through the `client` option.
30
29
*
31
30
* See the {@link https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/idempotency/ Idempotency documentation}
32
- * for more details on the Redis configuration and usage patterns.
31
+ * for more details on the configuration and usage patterns.
32
+ *
33
+ * **Using Valkey Glide Client**
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { GlideClient } from '@valkey/valkey-glide';
38
+ * import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
39
+ *
40
+ * const client = await GlideClient.createClient({
41
+ * addresses: [{
42
+ * host: process.env.CACHE_ENDPOINT,
43
+ * port: Number(process.env.CACHE_PORT),
44
+ * }],
45
+ * useTLS: true,
46
+ * });
47
+ *
48
+ * const persistence = new CachePersistenceLayer({
49
+ * client,
50
+ * });
51
+ *
52
+ * // ... your function handler here
53
+ * ```
54
+ *
55
+ * **Using Redis Client**
33
56
*
34
57
* @example
35
58
* ```ts
36
59
* import { createClient } from '@redis/client';
37
- * import { RedisPersistenceLayer } from '@aws-lambda-powertools/idempotency/redis';
38
- * import { RedisCompatibleClient } from '@aws-lambda-powertools/idempotency/redis/types';
60
+ * import { CachePersistenceLayer } from '@aws-lambda-powertools/idempotency/cache';
39
61
*
40
- * const redisClient = createClient({ url: 'redis://localhost:6379' });
41
- * await redisClient.connect();
62
+ * const client = await createClient({
63
+ * url: `rediss://${process.env.CACHE_ENDPOINT}:${process.env.CACHE_PORT}`,
64
+ * username: 'default',
65
+ * }).connect();
42
66
*
43
- * const persistence = new RedisPersistenceLayer ({
44
- * client: redisClient as RedisCompatibleClient ,
67
+ * const persistence = new CachePersistenceLayer ({
68
+ * client,
45
69
* });
70
+ *
71
+ * // ... your function handler here
46
72
* ```
47
73
*
48
74
* @category Persistence Layer
49
75
*/
50
- class RedisPersistenceLayer extends BasePersistenceLayer {
51
- readonly #client: RedisCompatibleClient ;
76
+ class CachePersistenceLayer extends BasePersistenceLayer {
77
+ readonly #client: CacheClient ;
52
78
readonly #dataAttr: string ;
53
79
readonly #expiryAttr: string ;
54
80
readonly #inProgressExpiryAttr: string ;
55
81
readonly #statusAttr: string ;
56
82
readonly #validationKeyAttr: string ;
57
83
readonly #orphanLockTimeout: number ;
58
84
59
- public constructor ( options : RedisPersistenceOptions ) {
85
+ public constructor ( options : CachePersistenceOptions ) {
60
86
super ( ) ;
61
87
62
88
this . #statusAttr =
@@ -76,9 +102,10 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
76
102
}
77
103
78
104
/**
79
- * Deletes the idempotency record associated with a given record from Redis.
105
+ * Deletes the idempotency record associated with a given record from the persistence store.
106
+ *
80
107
* This function is designed to be called after a Lambda handler invocation has completed processing.
81
- * It ensures that the idempotency key associated with the record is removed from Redis to
108
+ * It ensures that the idempotency key associated with the record is removed from the cache to
82
109
* prevent future conflicts and to maintain the idempotency integrity.
83
110
*
84
111
* Note: it is essential that the idempotency key is not empty, as that would indicate the Lambda
@@ -110,25 +137,24 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
110
137
'Item does not exist in persistence store'
111
138
) ;
112
139
}
113
- let item : JSONObject ;
114
140
try {
115
- item = JSON . parse ( response ) ;
141
+ const item = JSON . parse ( response as string ) ;
142
+ return new IdempotencyRecord ( {
143
+ idempotencyKey : idempotencyKey ,
144
+ status : item [ this . #statusAttr] as IdempotencyRecordStatusValue ,
145
+ expiryTimestamp : item [ this . #expiryAttr] as number | undefined ,
146
+ inProgressExpiryTimestamp : item [ this . #inProgressExpiryAttr] as
147
+ | number
148
+ | undefined ,
149
+ responseData : item [ this . #dataAttr] ,
150
+ payloadHash : item [ this . #validationKeyAttr] as string | undefined ,
151
+ } ) ;
116
152
} catch ( error ) {
117
153
throw new IdempotencyPersistenceConsistencyError (
118
154
'Idempotency persistency consistency error, needs to be removed' ,
119
155
error as Error
120
156
) ;
121
157
}
122
- return new IdempotencyRecord ( {
123
- idempotencyKey : idempotencyKey ,
124
- status : item [ this . #statusAttr] as IdempotencyRecordStatusValue ,
125
- expiryTimestamp : item [ this . #expiryAttr] as number | undefined ,
126
- inProgressExpiryTimestamp : item [ this . #inProgressExpiryAttr] as
127
- | number
128
- | undefined ,
129
- responseData : item [ this . #dataAttr] ,
130
- payloadHash : item [ this . #validationKeyAttr] as string | undefined ,
131
- } ) ;
132
158
}
133
159
134
160
protected async _updateRecord ( record : IdempotencyRecord ) : Promise < void > {
@@ -148,7 +174,8 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
148
174
149
175
/**
150
176
* Put a record in the persistence store with a status of "INPROGRESS".
151
- * The method guards against concurrent execution by using Redis' conditional write operations.
177
+ *
178
+ * The method guards against concurrent execution by using conditional write operations.
152
179
*/
153
180
async #putInProgressRecord( record : IdempotencyRecord ) : Promise < void > {
154
181
const item : Record < string , unknown > = {
@@ -180,9 +207,8 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
180
207
* The idempotency key does not exist:
181
208
* - first time that this invocation key is used
182
209
* - previous invocation with the same key was deleted due to TTL
183
- * - SET see https://redis .io/commands/set/
210
+ * - SET see { @link https://valkey .io/commands/set/ | Valkey SET command}
184
211
*/
185
-
186
212
const response = await this . #client. set (
187
213
record . idempotencyKey ,
188
214
encodedItem ,
@@ -193,7 +219,7 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
193
219
) ;
194
220
195
221
/**
196
- * If response is not `null`, the redis SET operation was successful and the idempotency key was not
222
+ * If response is not `null`, the SET operation was successful and the idempotency key was not
197
223
* previously set. This indicates that we can safely proceed to the handler execution phase.
198
224
* Most invocations should successfully proceed past this point.
199
225
*/
@@ -202,19 +228,21 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
202
228
}
203
229
204
230
/**
205
- * If response is `null`, it indicates an existing record in Redis for the given idempotency key.
231
+ * If response is `null`, it indicates an existing record in the cache for the given idempotency key.
232
+ *
206
233
* This could be due to:
207
234
* - An active idempotency record from a previous invocation that has not yet expired.
208
235
* - An orphan record where a previous invocation has timed out.
209
- * - An expired idempotency record that has not been deleted by Redis .
236
+ * - An expired idempotency record that has not been deleted yet .
210
237
*
211
238
* In any case, we proceed to retrieve the record for further inspection.
212
239
*/
213
240
const existingRecord = await this . _getRecord ( record . idempotencyKey ) ;
214
241
215
- /** If the status of the idempotency record is `COMPLETED` and the record has not expired
216
- * then a valid completed record exists. We raise an error to prevent duplicate processing
217
- * of a request that has already been completed successfully.
242
+ /**
243
+ * If the status of the idempotency record is `COMPLETED` and the record has not expired
244
+ * then a valid completed record exists. We raise an error to prevent duplicate processing
245
+ * of a request that has already been completed successfully.
218
246
*/
219
247
if (
220
248
existingRecord . getStatus ( ) === IdempotencyRecordStatus . COMPLETED &&
@@ -226,10 +254,11 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
226
254
) ;
227
255
}
228
256
229
- /** If the idempotency record has a status of 'INPROGRESS' and has a valid `inProgressExpiryTimestamp`
230
- * (meaning the timestamp is greater than the current timestamp in milliseconds), then we have encountered
231
- * a valid in-progress record. This indicates that another process is currently handling the request, and
232
- * to maintain idempotency, we raise an error to prevent concurrent processing of the same request.
257
+ /**
258
+ * If the idempotency record has a status of 'INPROGRESS' and has a valid `inProgressExpiryTimestamp`
259
+ * (meaning the timestamp is greater than the current timestamp in milliseconds), then we have encountered
260
+ * a valid in-progress record. This indicates that another process is currently handling the request, and
261
+ * to maintain idempotency, we raise an error to prevent concurrent processing of the same request.
233
262
*/
234
263
if (
235
264
existingRecord . getStatus ( ) === IdempotencyRecordStatus . INPROGRESS &&
@@ -242,20 +271,22 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
242
271
) ;
243
272
}
244
273
245
- /** Reaching this point indicates that the idempotency record found is an orphan record. An orphan record is
246
- * one that is neither completed nor in-progress within its expected time frame. It may result from a
247
- * previous invocation that has timed out or an expired record that has yet to be cleaned up by Redis.
248
- * We raise an error to handle this exceptional scenario appropriately.
274
+ /**
275
+ * Reaching this point indicates that the idempotency record found is an orphan record. An orphan record is
276
+ * one that is neither completed nor in-progress within its expected time frame. It may result from a
277
+ * previous invocation that has timed out or an expired record that has yet to be cleaned up from the cache.
278
+ * We raise an error to handle this exceptional scenario appropriately.
249
279
*/
250
280
throw new IdempotencyPersistenceConsistencyError (
251
281
'Orphaned record detected'
252
282
) ;
253
283
} catch ( error ) {
254
284
if ( error instanceof IdempotencyPersistenceConsistencyError ) {
255
- /** Handle an orphan record by attempting to acquire a lock, which by default lasts for 10 seconds.
256
- * The purpose of acquiring the lock is to prevent race conditions with other processes that might
257
- * also be trying to handle the same orphan record. Once the lock is acquired, we set a new value
258
- * for the idempotency record in Redis with the appropriate time-to-live (TTL).
285
+ /**
286
+ * Handle an orphan record by attempting to acquire a lock, which by default lasts for 10 seconds.
287
+ * The purpose of acquiring the lock is to prevent race conditions with other processes that might
288
+ * also be trying to handle the same orphan record. Once the lock is acquired, we set a new value
289
+ * for the idempotency record in the cache with the appropriate time-to-live (TTL).
259
290
*/
260
291
await this . #acquireLock( record . idempotencyKey ) ;
261
292
@@ -280,7 +311,7 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
280
311
281
312
/**
282
313
* Attempt to acquire a lock for a specified resource name, with a default timeout.
283
- * This method attempts to set a lock using Redis to prevent concurrent access to a resource
314
+ * This method attempts to set a lock to prevent concurrent access to a resource
284
315
* identified by 'idempotencyKey'. It uses the 'NX' flag to ensure that the lock is only
285
316
* set if it does not already exist, thereby enforcing mutual exclusion.
286
317
*
@@ -306,4 +337,4 @@ class RedisPersistenceLayer extends BasePersistenceLayer {
306
337
}
307
338
}
308
339
309
- export { RedisPersistenceLayer } ;
340
+ export { CachePersistenceLayer } ;
0 commit comments