-
Notifications
You must be signed in to change notification settings - Fork 146
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
feat(idempotency): Add function wrapper and decorator #1262
Changes from 72 commits
51104f6
d54e22e
6ea753e
c5f2144
ab254c6
8677376
b955ac4
c8776d2
f258d55
edc9b33
dfc72c0
4d63218
d92efb1
5258d10
5d82215
95004c2
c5a9335
3d1f52c
0a6e839
b9ae754
2cd4390
01bcc99
c1a6c60
1011002
adcfab3
9f0535e
9bf7885
ffb2c22
ec31dbb
48dfdec
f666dfa
0945801
e161521
9efeae0
0fea9f3
b4165bd
d1dad17
b9afc2a
0ec8301
1366614
38d7a67
dd442e2
8e97a61
afb9523
8a031ca
f476e21
a044fa6
cefae2d
dd979a8
b290d19
f76c720
b4931bf
9b4787a
08598f9
b3fe0be
0af6a5e
68ce9a2
19e01eb
5afab50
78f1a89
58e1bda
8fd2436
2e43938
9e369f7
cdae838
f5e3b6b
927654f
d0d0574
1ec1c80
e3eb775
bd2388c
eadb129
780a8ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,4 +86,4 @@ | |
"dependencies": { | ||
"hosted-git-info": "^6.1.1" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
||
import { AnyFunctionWithRecord, IdempotencyRecordStatus } from './types'; | ||
import { IdempotencyOptions } from './types/IdempotencyOptions'; | ||
import { IdempotencyRecord } from 'persistence'; | ||
import { IdempotencyInconsistentStateError, IdempotencyItemAlreadyExistsError, IdempotencyAlreadyInProgressError, IdempotencyPersistenceLayerError } from './Exceptions'; | ||
|
||
export class IdempotencyHandler<U> { | ||
|
||
public constructor(private functiontoMakeIdempotent: AnyFunctionWithRecord<U>, private functionPayloadToBeHashed: unknown, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion for this constructor signature: Question: why the explicit access modifiers in the parameters? (private) |
||
private idempotencyOptions: IdempotencyOptions, private fullFunctionPayload: Record<string, any>) {} | ||
|
||
public determineResultFromIdempotencyRecord(idempotencyRecord: IdempotencyRecord): Promise<U> | U{ | ||
if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.EXPIRED) { | ||
throw new IdempotencyInconsistentStateError(); | ||
} else if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.INPROGRESS){ | ||
throw new IdempotencyAlreadyInProgressError(`There is already an execution in progress with idempotency key: ${idempotencyRecord.idempotencyKey}`); | ||
} else { | ||
// Currently recalling the method as this fulfills FR1. FR3 will address using the previously stored value https://github.com/awslabs/aws-lambda-powertools-typescript/issues/447 | ||
return this.functiontoMakeIdempotent(this.fullFunctionPayload); | ||
} | ||
} | ||
|
||
public async process_idempotency(): Promise<U> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you use camelCase for consistency? |
||
try { | ||
await this.idempotencyOptions.persistenceStore.saveInProgress(this.functionPayloadToBeHashed); | ||
} catch (e) { | ||
if (e instanceof IdempotencyItemAlreadyExistsError) { | ||
const idempotencyRecord: IdempotencyRecord = await this.idempotencyOptions.persistenceStore.getRecord(this.functionPayloadToBeHashed); | ||
|
||
return this.determineResultFromIdempotencyRecord(idempotencyRecord); | ||
} else { | ||
throw new IdempotencyPersistenceLayerError(); | ||
} | ||
} | ||
|
||
return this.functiontoMakeIdempotent(this.fullFunctionPayload); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { IdempotencyOptions } from './types/IdempotencyOptions'; | ||
import { IdempotencyHandler } from './IdempotencyHandler'; | ||
|
||
const idempotent = function (options: IdempotencyOptions) { | ||
return function (_target: any, _propertyKey: string, descriptor: PropertyDescriptor) { | ||
const childFunction = descriptor.value; | ||
descriptor.value = function(record: Record<string, any>){ | ||
const idempotencyHandler: IdempotencyHandler<unknown> = new IdempotencyHandler<unknown>(childFunction, record[options.dataKeywordArgument], options, record); | ||
|
||
return idempotencyHandler.process_idempotency(); | ||
}; | ||
|
||
return descriptor; | ||
}; | ||
}; | ||
|
||
export { idempotent }; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,19 @@ | ||
import type { AnyFunction } from './types/AnyFunction'; | ||
import type { IdempotencyOptions } from './types/IdempotencyOptions'; | ||
|
||
const makeFunctionIdempotent = <U>( | ||
fn: AnyFunction<U>, | ||
_options: IdempotencyOptions | ||
// TODO: revisit this with a more specific type if possible | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
): (...args: Array<any>) => Promise<U | void> => (...args) => fn(...args); | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { AnyFunctionWithRecord, AnyIdempotentFunction } from './types/AnyFunction'; | ||
import { IdempotencyOptions } from './types/IdempotencyOptions'; | ||
import { IdempotencyHandler } from './IdempotencyHandler'; | ||
|
||
const makeFunctionIdempotent = function <U>( | ||
fn: AnyFunctionWithRecord<U>, | ||
saragerion marked this conversation as resolved.
Show resolved
Hide resolved
|
||
options: IdempotencyOptions | ||
): AnyIdempotentFunction<U> { | ||
const wrappedFunction: AnyIdempotentFunction<U> = function (record: Record<string, any>): Promise<U> { | ||
const idempotencyHandler: IdempotencyHandler<U> = new IdempotencyHandler<U>(fn, record[options.dataKeywordArgument], options, record); | ||
|
||
return idempotencyHandler.process_idempotency(); | ||
}; | ||
|
||
return wrappedFunction; | ||
}; | ||
|
||
export { makeFunctionIdempotent }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,11 @@ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
type AnyFunction<U> = (...args: Array<any>) => Promise<U>; | ||
type AnyFunctionWithRecord<U> = (record: Record<string,any>) => Promise<U> | U; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
type AnyIdempotentFunction<U> = (record: Record<string,any>) => Promise<U>; | ||
|
||
export { | ||
AnyFunction | ||
// AnyFunction, | ||
AnyFunctionWithRecord, | ||
AnyIdempotentFunction | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
export * from './AnyFunction'; | ||
export * from './IdempotencyRecordStatus'; | ||
export * from './PersistenceLayer'; | ||
export * from './IdempotencyRecordOptions'; | ||
export * from './PersistenceLayer'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: adding a meaningful and easy to understand error message for each of these Errors would improve the DX and help troubleshooting errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a message to this when the error is created since this error can happen in more than one type of scenario. That way we can tailor the message by the scenario