Skip to content

Commit a558f10

Browse files
authored
feat(idempotency): makeHandlerIdempotent middy middleware (#1474)
* feat: makeHandlerIdempotent middy middleware
1 parent faa9307 commit a558f10

12 files changed

+577
-42
lines changed

Diff for: packages/idempotency/src/IdempotencyConfig.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { EnvironmentVariablesService } from './config';
12
import type { Context } from 'aws-lambda';
23
import type { IdempotencyConfigOptions } from './types';
34

@@ -10,6 +11,8 @@ class IdempotencyConfig {
1011
public payloadValidationJmesPath?: string;
1112
public throwOnNoIdempotencyKey: boolean;
1213
public useLocalCache: boolean;
14+
readonly #envVarsService: EnvironmentVariablesService;
15+
readonly #enabled: boolean = true;
1316

1417
public constructor(config: IdempotencyConfigOptions) {
1518
this.eventKeyJmesPath = config.eventKeyJmesPath ?? '';
@@ -20,6 +23,17 @@ class IdempotencyConfig {
2023
this.maxLocalCacheSize = config.maxLocalCacheSize ?? 1000;
2124
this.hashFunction = config.hashFunction ?? 'md5';
2225
this.lambdaContext = config.lambdaContext;
26+
this.#envVarsService = new EnvironmentVariablesService();
27+
this.#enabled = this.#envVarsService.getIdempotencyEnabled();
28+
}
29+
30+
/**
31+
* Determines if the idempotency feature is enabled.
32+
*
33+
* @returns {boolean} Returns true if the idempotency feature is enabled.
34+
*/
35+
public isEnabled(): boolean {
36+
return this.#enabled;
2337
}
2438

2539
public registerLambdaContext(context: Context): void {

Diff for: packages/idempotency/src/IdempotencyHandler.ts

+22-15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from './Exceptions';
99
import { BasePersistenceLayer, IdempotencyRecord } from './persistence';
1010
import { IdempotencyConfig } from './IdempotencyConfig';
11+
import { MAX_RETRIES } from './constants';
1112

1213
export class IdempotencyHandler<U> {
1314
private readonly fullFunctionPayload: Record<string, unknown>;
@@ -36,9 +37,9 @@ export class IdempotencyHandler<U> {
3637
});
3738
}
3839

39-
public determineResultFromIdempotencyRecord(
40+
public static determineResultFromIdempotencyRecord(
4041
idempotencyRecord: IdempotencyRecord
41-
): Promise<U> | U {
42+
): Promise<unknown> | unknown {
4243
if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.EXPIRED) {
4344
throw new IdempotencyInconsistentStateError(
4445
'Item has expired during processing and may not longer be valid.'
@@ -61,7 +62,7 @@ export class IdempotencyHandler<U> {
6162
}
6263
}
6364

64-
return idempotencyRecord.getResponse() as U;
65+
return idempotencyRecord.getResponse();
6566
}
6667

6768
public async getFunctionResult(): Promise<U> {
@@ -96,26 +97,30 @@ export class IdempotencyHandler<U> {
9697

9798
/**
9899
* Main entry point for the handler
99-
* IdempotencyInconsistentStateError can happen under rare but expected cases
100-
* when persistent state changes in the small time between put & get requests.
101-
* In most cases we can retry successfully on this exception.
100+
*
101+
* In some rare cases, when the persistent state changes in small time
102+
* window, we might get an `IdempotencyInconsistentStateError`. In such
103+
* cases we can safely retry the handling a few times.
102104
*/
103105
public async handle(): Promise<U> {
104-
const MAX_RETRIES = 2;
105-
for (let i = 1; i <= MAX_RETRIES; i++) {
106+
let e;
107+
for (let retryNo = 0; retryNo <= MAX_RETRIES; retryNo++) {
106108
try {
107109
return await this.processIdempotency();
108-
} catch (e) {
110+
} catch (error) {
109111
if (
110-
!(e instanceof IdempotencyAlreadyInProgressError) ||
111-
i === MAX_RETRIES
112+
error instanceof IdempotencyInconsistentStateError &&
113+
retryNo < MAX_RETRIES
112114
) {
113-
throw e;
115+
// Retry
116+
continue;
114117
}
118+
// Retries exhausted or other error
119+
e = error;
120+
break;
115121
}
116122
}
117-
/* istanbul ignore next */
118-
throw new Error('This should never happen');
123+
throw e;
119124
}
120125

121126
public async processIdempotency(): Promise<U> {
@@ -128,7 +133,9 @@ export class IdempotencyHandler<U> {
128133
const idempotencyRecord: IdempotencyRecord =
129134
await this.persistenceStore.getRecord(this.functionPayloadToBeHashed);
130135

131-
return this.determineResultFromIdempotencyRecord(idempotencyRecord);
136+
return IdempotencyHandler.determineResultFromIdempotencyRecord(
137+
idempotencyRecord
138+
) as U;
132139
} else {
133140
throw new IdempotencyPersistenceLayerError();
134141
}

Diff for: packages/idempotency/src/config/ConfigServiceInterface.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ interface ConfigServiceInterface {
44
getServiceName(): string;
55

66
getFunctionName(): string;
7+
8+
getIdempotencyEnabled(): boolean;
79
}
810

911
export { ConfigServiceInterface };

Diff for: packages/idempotency/src/config/EnvironmentVariablesService.ts

+12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class EnvironmentVariablesService
2121
{
2222
// Reserved environment variables
2323
private functionNameVariable = 'AWS_LAMBDA_FUNCTION_NAME';
24+
private idempotencyDisabledVariable = 'POWERTOOLS_IDEMPOTENCY_DISABLED';
2425

2526
/**
2627
* It returns the value of the AWS_LAMBDA_FUNCTION_NAME environment variable.
@@ -30,6 +31,17 @@ class EnvironmentVariablesService
3031
public getFunctionName(): string {
3132
return this.get(this.functionNameVariable);
3233
}
34+
35+
/**
36+
* It returns whether the idempotency feature is enabled or not.
37+
*
38+
* Reads the value of the POWERTOOLS_IDEMPOTENCY_DISABLED environment variable.
39+
*
40+
* @returns {boolean}
41+
*/
42+
public getIdempotencyEnabled(): boolean {
43+
return !this.isValueTrue(this.get(this.idempotencyDisabledVariable));
44+
}
3345
}
3446

3547
export { EnvironmentVariablesService };

Diff for: packages/idempotency/src/constants.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Number of times to retry a request in case of `IdempotencyInconsistentStateError`
3+
*
4+
* Used in `IdempotencyHandler` and `makeHandlerIdempotent`
5+
*
6+
* @internal
7+
*/
8+
const MAX_RETRIES = 2;
9+
10+
export { MAX_RETRIES };

Diff for: packages/idempotency/src/makeFunctionIdempotent.ts

+26-9
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,58 @@
1+
import type { Context } from 'aws-lambda';
12
import type {
23
AnyFunctionWithRecord,
34
AnyIdempotentFunction,
4-
GenericTempRecord,
55
IdempotencyFunctionOptions,
66
} from './types';
77
import { IdempotencyHandler } from './IdempotencyHandler';
88
import { IdempotencyConfig } from './IdempotencyConfig';
99

10+
const isContext = (arg: unknown): arg is Context => {
11+
return (
12+
arg !== undefined &&
13+
arg !== null &&
14+
typeof arg === 'object' &&
15+
'getRemainingTimeInMillis' in arg
16+
);
17+
};
18+
1019
const makeFunctionIdempotent = function <U>(
1120
fn: AnyFunctionWithRecord<U>,
1221
options: IdempotencyFunctionOptions
13-
): AnyIdempotentFunction<U> {
22+
): AnyIdempotentFunction<U> | AnyFunctionWithRecord<U> {
23+
const idempotencyConfig = options.config
24+
? options.config
25+
: new IdempotencyConfig({});
26+
1427
const wrappedFn: AnyIdempotentFunction<U> = function (
15-
record: GenericTempRecord
28+
...args: Parameters<AnyFunctionWithRecord<U>>
1629
): Promise<U> {
30+
const payload = args[0];
31+
const context = args[1];
32+
1733
if (options.dataKeywordArgument === undefined) {
1834
throw new Error(
1935
`Missing data keyword argument ${options.dataKeywordArgument}`
2036
);
2137
}
22-
const idempotencyConfig = options.config
23-
? options.config
24-
: new IdempotencyConfig({});
38+
if (isContext(context)) {
39+
idempotencyConfig.registerLambdaContext(context);
40+
}
2541
const idempotencyHandler: IdempotencyHandler<U> = new IdempotencyHandler<U>(
2642
{
2743
functionToMakeIdempotent: fn,
28-
functionPayloadToBeHashed: record[options.dataKeywordArgument],
44+
functionPayloadToBeHashed: payload[options.dataKeywordArgument],
2945
idempotencyConfig: idempotencyConfig,
3046
persistenceStore: options.persistenceStore,
31-
fullFunctionPayload: record,
47+
fullFunctionPayload: payload,
3248
}
3349
);
3450

3551
return idempotencyHandler.handle();
3652
};
3753

38-
return wrappedFn;
54+
if (idempotencyConfig.isEnabled()) return wrappedFn;
55+
else return fn;
3956
};
4057

4158
export { makeFunctionIdempotent };

Diff for: packages/idempotency/src/middleware/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './makeHandlerIdempotent';

0 commit comments

Comments
 (0)