-
Notifications
You must be signed in to change notification settings - Fork 146
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(idempotency): add idempotency decorator (#1723)
- Loading branch information
Showing
8 changed files
with
1,121 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import type { Context } from 'aws-lambda'; | ||
import { LambdaInterface } from '@aws-lambda-powertools/commons'; | ||
import { | ||
IdempotencyConfig, | ||
idempotent, | ||
} from '@aws-lambda-powertools/idempotency'; | ||
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb'; | ||
import { Request, Response } from './types'; | ||
|
||
const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer({ | ||
tableName: 'idempotencyTableName', | ||
}); | ||
|
||
const config = new IdempotencyConfig({}); | ||
|
||
class MyLambda implements LambdaInterface { | ||
@idempotent({ persistenceStore: dynamoDBPersistenceLayer, config: config }) | ||
public async handler(_event: Request, _context: Context): Promise<Response> { | ||
// ... process your event | ||
return { | ||
message: 'success', | ||
statusCode: 200, | ||
}; | ||
} | ||
} | ||
|
||
const defaultLambda = new MyLambda(); | ||
export const handler = defaultLambda.handler.bind(defaultLambda); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { AnyFunction, ItempotentFunctionOptions } from './types'; | ||
import { makeIdempotent } from './makeIdempotent'; | ||
|
||
/** | ||
* Use this decorator to make your lambda handler itempotent. | ||
* You need to provide a peristance layer to store the idempotency information. | ||
* At the moment we only support `DynamodbPersistenceLayer`. | ||
* | ||
* @example | ||
* ```ts | ||
* import { | ||
* DynamoDBPersistenceLayer, | ||
* idempotentLambdaHandler | ||
* } from '@aws-lambda-powertools/idempotency' | ||
* | ||
* class MyLambdaFunction { | ||
* @idempotent({ persistenceStore: new DynamoDBPersistenceLayer() }) | ||
* async handler(event: any, context: any) { | ||
* return "Hello World"; | ||
* } | ||
* } | ||
* export myLambdaHandler new MyLambdaFunction(); | ||
* export const handler = myLambdaHandler.handler.bind(myLambdaHandler); | ||
* ``` | ||
* | ||
* Similar to decoratoring a handler you can use the decorator on any other function. | ||
* @example | ||
* ```ts | ||
* import { | ||
* DynamoDBPersistenceLayer, | ||
* idempotentFunction | ||
* } from '@aws-lambda-powertools/idempotency' | ||
* | ||
* class MyClass { | ||
* | ||
* public async handler(_event: any, _context: any) { | ||
* for(const record of _event.records){ | ||
* await this.process(record); | ||
* } | ||
* } | ||
* | ||
* @idemptent({ persistenceStore: new DynamoDBPersistenceLayer() }) | ||
* public async process(record: Record<stiring, unknown) { | ||
* // do some processing | ||
* } | ||
* ``` | ||
* @see {@link DynamoDBPersistenceLayer} | ||
* @see https://www.typescriptlang.org/docs/handbook/decorators.html | ||
*/ | ||
const idempotent = function ( | ||
options: ItempotentFunctionOptions<Parameters<AnyFunction>> | ||
): ( | ||
target: unknown, | ||
propertyKey: string, | ||
descriptor: PropertyDescriptor | ||
) => PropertyDescriptor { | ||
return function ( | ||
_target: unknown, | ||
_propertyKey: string, | ||
descriptor: PropertyDescriptor | ||
) { | ||
const childFunction = descriptor.value; | ||
descriptor.value = makeIdempotent(childFunction, options); | ||
|
||
return descriptor; | ||
}; | ||
}; | ||
|
||
export { idempotent }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
export * from './errors'; | ||
export * from './IdempotencyConfig'; | ||
export * from './makeIdempotent'; | ||
export * from './idempotencyDecorator'; | ||
export { IdempotencyRecordStatus } from './constants'; |
164 changes: 164 additions & 0 deletions
164
packages/idempotency/tests/e2e/idempotentDecorator.test.FunctionCode.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import type { Context } from 'aws-lambda'; | ||
import { LambdaInterface } from '@aws-lambda-powertools/commons'; | ||
import { idempotent } from '../../src'; | ||
import { Logger } from '../../../logger'; | ||
import { DynamoDBPersistenceLayer } from '../../src/persistence/DynamoDBPersistenceLayer'; | ||
import { IdempotencyConfig } from '../../src/'; | ||
|
||
const IDEMPOTENCY_TABLE_NAME = | ||
process.env.IDEMPOTENCY_TABLE_NAME || 'table_name'; | ||
const dynamoDBPersistenceLayer = new DynamoDBPersistenceLayer({ | ||
tableName: IDEMPOTENCY_TABLE_NAME, | ||
}); | ||
|
||
const dynamoDBPersistenceLayerCustomized = new DynamoDBPersistenceLayer({ | ||
tableName: IDEMPOTENCY_TABLE_NAME, | ||
dataAttr: 'dataAttr', | ||
keyAttr: 'customId', | ||
expiryAttr: 'expiryAttr', | ||
statusAttr: 'statusAttr', | ||
inProgressExpiryAttr: 'inProgressExpiryAttr', | ||
staticPkValue: 'staticPkValue', | ||
validationKeyAttr: 'validationKeyAttr', | ||
}); | ||
|
||
const config = new IdempotencyConfig({}); | ||
|
||
class DefaultLambda implements LambdaInterface { | ||
@idempotent({ persistenceStore: dynamoDBPersistenceLayer }) | ||
public async handler( | ||
_event: Record<string, unknown>, | ||
_context: Context | ||
): Promise<string> { | ||
logger.info(`Got test event: ${JSON.stringify(_event)}`); | ||
// sleep to enforce error with parallel execution | ||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
|
||
return 'Hello World'; | ||
} | ||
|
||
@idempotent({ | ||
persistenceStore: dynamoDBPersistenceLayerCustomized, | ||
config: config, | ||
}) | ||
public async handlerCustomized( | ||
event: { foo: string }, | ||
context: Context | ||
): Promise<string> { | ||
config.registerLambdaContext(context); | ||
logger.info('Processed event', { details: event.foo }); | ||
|
||
return event.foo; | ||
} | ||
|
||
@idempotent({ | ||
persistenceStore: dynamoDBPersistenceLayer, | ||
config: new IdempotencyConfig({ | ||
useLocalCache: false, | ||
expiresAfterSeconds: 1, | ||
eventKeyJmesPath: 'foo', | ||
}), | ||
}) | ||
public async handlerExpired( | ||
event: { foo: string; invocation: number }, | ||
context: Context | ||
): Promise<{ foo: string; invocation: number }> { | ||
logger.addContext(context); | ||
|
||
logger.info('Processed event', { details: event.foo }); | ||
|
||
return { | ||
foo: event.foo, | ||
invocation: event.invocation, | ||
}; | ||
} | ||
|
||
@idempotent({ persistenceStore: dynamoDBPersistenceLayer }) | ||
public async handlerParallel( | ||
event: { foo: string }, | ||
context: Context | ||
): Promise<string> { | ||
logger.addContext(context); | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 1500)); | ||
|
||
logger.info('Processed event', { details: event.foo }); | ||
|
||
return event.foo; | ||
} | ||
|
||
@idempotent({ | ||
persistenceStore: dynamoDBPersistenceLayer, | ||
config: new IdempotencyConfig({ | ||
eventKeyJmesPath: 'foo', | ||
}), | ||
}) | ||
public async handlerTimeout( | ||
event: { foo: string; invocation: number }, | ||
context: Context | ||
): Promise<{ foo: string; invocation: number }> { | ||
logger.addContext(context); | ||
|
||
if (event.invocation === 0) { | ||
await new Promise((resolve) => setTimeout(resolve, 4000)); | ||
} | ||
|
||
logger.info('Processed event', { | ||
details: event.foo, | ||
}); | ||
|
||
return { | ||
foo: event.foo, | ||
invocation: event.invocation, | ||
}; | ||
} | ||
} | ||
|
||
const defaultLambda = new DefaultLambda(); | ||
const handler = defaultLambda.handler.bind(defaultLambda); | ||
const handlerParallel = defaultLambda.handlerParallel.bind(defaultLambda); | ||
|
||
const handlerCustomized = defaultLambda.handlerCustomized.bind(defaultLambda); | ||
|
||
const handlerTimeout = defaultLambda.handlerTimeout.bind(defaultLambda); | ||
|
||
const handlerExpired = defaultLambda.handlerExpired.bind(defaultLambda); | ||
|
||
const logger = new Logger(); | ||
|
||
class LambdaWithKeywordArgument implements LambdaInterface { | ||
public async handler( | ||
event: { id: string }, | ||
_context: Context | ||
): Promise<string> { | ||
config.registerLambdaContext(_context); | ||
await this.process(event.id, 'bar'); | ||
|
||
return 'Hello World Keyword Argument'; | ||
} | ||
|
||
@idempotent({ | ||
persistenceStore: dynamoDBPersistenceLayer, | ||
config: config, | ||
dataIndexArgument: 1, | ||
}) | ||
public async process(id: string, foo: string): Promise<string> { | ||
logger.info('Got test event', { id, foo }); | ||
|
||
return 'idempotent result: ' + foo; | ||
} | ||
} | ||
|
||
const handlerDataIndexArgument = new LambdaWithKeywordArgument(); | ||
const handlerWithKeywordArgument = handlerDataIndexArgument.handler.bind( | ||
handlerDataIndexArgument | ||
); | ||
|
||
export { | ||
handler, | ||
handlerCustomized, | ||
handlerExpired, | ||
handlerWithKeywordArgument, | ||
handlerTimeout, | ||
handlerParallel, | ||
}; |
Oops, something went wrong.