Skip to content

Commit

Permalink
feat(idempotency): Add function wrapper and decorator (#1262)
Browse files Browse the repository at this point in the history
* feat: initial idempotency classes

* feat: refactor persistence layer classes into their own folder

* feat: rename idempotency config to differentiate from idempotency options

* feat: added type for a generic function

* feat: remove idempotency configuration for this FR

* feat: refactored type of function to accept any combo of parameters

* feat: adding PersistenceLayer

* feat: PersistenceLayer unit tests for saveInProgress

* feat: added saveSuccess

* feat: added getRecord

* feat: added delete record

* feat: branch coverage and cleaning up imports

* feat: added more tests

* feat: deleted unused methods

* feat: added comments

* feat: implement get command for dynamo persistence layer

* feat: implement get command for dynamo persistence layer

* feat: allow for data attr to be passed and return in persistence layer get

* feat: added implementation for delete, update, put

* feat: create condition on put for not in progress status

* feat: use inprogress enum for status

* feat: added error when unable to get record for idempotency key

* feat: added error for conditional write of an existing record

* feat: tests added for put record on dynamo persistence layer

* feat: implemented the idempotency record functions for status, expiry, and json response

* test: check if the status is expired

* test: idempotency record is not expired and status maintained

* feat: added tests for get record

* feat: add aws-sdk-client-mock jest assertion library

* feat: add unit tests for update record and delete record

* feat: remove optional chaining from item made unnecessary with error branch

* feat: remove unused block

* feat: refactored mock child class to be shared amongst dynamo persistence layer tests

* test: add path to get the response data from the data record

* feat: added branch to handle conditional check failure

* feat: add configuration option to dynamo client creation to remove undefined values

* feat: change how time is measured to seconds

* feat: change type of the response/result to a record

* feat:updated imports

* feat: added save in progress to handle already existing records in dynamo

* feat: add log message for the already in progress error

* feat: change the anyfunction type definition to also include a sync function

* refactor: create constructor object for dynamo persistence layer

* fix: remove temp eslint disable

* fix: adjust verbiage on test blocks

Co-authored-by: ijemmy <ijemmy@users.noreply.github.com>

* style: put constructor parameters onto one line for readability

* fix:  update dynamo persistence layer tests to use new construtor options

* fix: remove unneeded eslint ignore from persistence layer

* style: put parameters for dynamo client command object onto one line for readability

* fix: move lib-dynamo dep under the correct package

* refactor: change idempotency record to use options object in contructor

* feat: add consistent read to dynamo persistence layer

* fix: revert changes to layer-publisher package-lock

* feat added the call to the function in the idempotency handler and add overarching error scenario

* feat: add logic to invoke function if it has not already been called; add logic to call the idempotency hander

* chore: move and enhance comment on question

* chore: update comments

* feat: use new record formatting for idempotent function wrapper

* test: add test case for issues even getting to save the record in persistence layer

* chore: refactoring test suite for idempotent wrapper

* chore: clean up comments

* feat: added decorator for idempotency

* chore: get rid of extra private member on the idempotency handler class

* chore: refactor to use class for options

* chore: bring in old version of package-locks

* chore: update paths for interface

* chore: remove env files

* chore: rename file

* chore: renaming test names and function names for idempotency decorator/wrapper

---------

Co-authored-by: vgphoenixcampos <111440293+vgphoenixcampos@users.noreply.github.com>
Co-authored-by: jeffrey-baker-vg <jeffrey_w-m_baker@vanguard.com>
Co-authored-by: Phoenix Campos <phoenix_campos@vanguard.com>
Co-authored-by: ijemmy <ijemmy@users.noreply.github.com>
  • Loading branch information
5 people committed Feb 6, 2023
1 parent 3a8cfa0 commit eacb1d9
Show file tree
Hide file tree
Showing 12 changed files with 444 additions and 29 deletions.
29 changes: 16 additions & 13 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -86,4 +86,4 @@
"dependencies": {
"hosted-git-info": "^6.1.1"
}
}
}
17 changes: 16 additions & 1 deletion packages/idempotency/src/Exceptions.ts
Expand Up @@ -10,8 +10,23 @@ class IdempotencyInvalidStatusError extends Error {

}

class IdempotencyInconsistentStateError extends Error {

}

class IdempotencyAlreadyInProgressError extends Error {

}

class IdempotencyPersistenceLayerError extends Error {

}

export {
IdempotencyItemNotFoundError,
IdempotencyItemAlreadyExistsError,
IdempotencyInvalidStatusError
IdempotencyInvalidStatusError,
IdempotencyInconsistentStateError,
IdempotencyAlreadyInProgressError,
IdempotencyPersistenceLayerError
};
39 changes: 39 additions & 0 deletions packages/idempotency/src/IdempotencyHandler.ts
@@ -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,
private idempotencyOptions: IdempotencyOptions, private fullFunctionPayload: Record<string, any>) {}

public determineResultFromIdempotencyRecord(idempotencyRecord: IdempotencyRecord): Promise<U> | U{
if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.EXPIRED) {
throw new IdempotencyInconsistentStateError('Item has expired during processing and may not longer be valid.');
} 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 processIdempotency(): Promise<U> {
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);
}
}
19 changes: 19 additions & 0 deletions packages/idempotency/src/idempotentDecorator.ts
@@ -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.processIdempotency();
};

return descriptor;
};
};

export { idempotent };

26 changes: 17 additions & 9 deletions packages/idempotency/src/makeFunctionIdempotent.ts
@@ -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>,
options: IdempotencyOptions
): AnyIdempotentFunction<U> {
const wrappedFn: AnyIdempotentFunction<U> = function (record: Record<string, any>): Promise<U> {
const idempotencyHandler: IdempotencyHandler<U> = new IdempotencyHandler<U>(fn, record[options.dataKeywordArgument], options, record);

return idempotencyHandler.processIdempotency();
};

return wrappedFn;
};

export { makeFunctionIdempotent };
9 changes: 7 additions & 2 deletions packages/idempotency/src/types/AnyFunction.ts
@@ -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
};
3 changes: 2 additions & 1 deletion packages/idempotency/src/types/index.ts
@@ -1,3 +1,4 @@
export * from './AnyFunction';
export * from './IdempotencyRecordStatus';
export * from './PersistenceLayer';
export * from './IdempotencyRecordOptions';
export * from './PersistenceLayer';
Expand Up @@ -6,4 +6,4 @@ process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE = '128';
if (process.env.AWS_REGION === undefined && process.env.CDK_DEFAULT_REGION === undefined) {
process.env.AWS_REGION = 'eu-west-1';
}
process.env._HANDLER = 'index.handler';
process.env._HANDLER = 'index.handler';

0 comments on commit eacb1d9

Please sign in to comment.