From 86c476fe9d1f621159f46d63570e7c2317db91db Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 10 Feb 2022 11:35:41 +0100 Subject: [PATCH 1/5] feat: added Utility common class w/ cold start heuristic + tests --- packages/commons/src/Utility.ts | 76 ++++++++++ packages/commons/tests/unit/Utility.test.ts | 145 ++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 packages/commons/src/Utility.ts create mode 100644 packages/commons/tests/unit/Utility.test.ts diff --git a/packages/commons/src/Utility.ts b/packages/commons/src/Utility.ts new file mode 100644 index 0000000000..7a08ba698e --- /dev/null +++ b/packages/commons/src/Utility.ts @@ -0,0 +1,76 @@ +/** + * ## Intro + * Utility is a base class that other Powertools utilites can extend to inherit shared logic. + * + * + * ## Key features + * * Cold Start heuristic to determine if the current + * + * ## Usage + * + * ### Cold Start + * + * Cold start is a term commonly used to describe the `Init` phase of a Lambda function. In this phase, Lambda creates or unfreezes an execution environment with the configured resources, downloads the code for the function and all layers, initializes any extensions, initializes the runtime, and then runs the function’s initialization code (the code outside the main handler). The Init phase happens either during the first invocation, or in advance of function invocations if you have enabled provisioned concurrency. + * + * To learn more about the Lambda execution environment lifecycle, see the [Execution environment section](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html) of the AWS Lambda documentation. + * + * As a Powertools user you probably won't be using this class directly, in fact if you use other Powertools utilities the cold start heuristic found here is already used to: + * * Add a `coldStart` key to the structured logs when injecting context information in `Logger` + * * Emit a metric during a cold start function invocation in ``Metrics` + * * Annotate the invocation segment with a `coldStart` key in `Tracer` + * + * If instead you want use this logic in your own utilities, `Utility` provides two methdos: + * + * #### `getColdStart()` + * + * Since the `Utility` class is instantiated outside of the Lambda handler it will persist across invocations of the same execution environment. This means that if you call `getColdStart()` multiple times, it will return `true` during the first invocation, and `false` afterwards. + * + * @example + * ```typescript + * import { Utility } from '@aws-lambda-powertools/commons'; + * + * const utility = new Utility(); + * + * export const handler = async (_event: any, _context: any) => { + * utility.getColdStart(); + * }; + * ``` + * + * #### `isColdStart()` + * + * This method is an alias of `getColdStart()` and is exposed for convenience and better readability in certain usages. + * + * @example + * ```typescript + * import { Utility } from '@aws-lambda-powertools/commons'; + * + * const utility = new Utility(); + * + * export const handler = async (_event: any, _context: any) => { + * if (utility.isColdStart()) { + * // do something, this block is only executed on the first invocation of the function + * } else { + * // do something else, this block gets executed on all subsequent invocations + * } + * }; + * ``` + */ +export class Utility { + + private coldStart: boolean = true; + + public getColdStart(): boolean { + if (this.coldStart) { + this.coldStart = false; + + return true; + } + + return false; + } + + public isColdStart(): boolean { + return this.getColdStart(); + } + +} \ No newline at end of file diff --git a/packages/commons/tests/unit/Utility.test.ts b/packages/commons/tests/unit/Utility.test.ts new file mode 100644 index 0000000000..42db79d7af --- /dev/null +++ b/packages/commons/tests/unit/Utility.test.ts @@ -0,0 +1,145 @@ +/** + * Test Utility class + * + * @group unit/commons/utility + */ +import { Utility } from '../../src'; + +describe('Class: Utility', () => { + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + describe('Method: getColdStart', () => { + + test('when called multiple times on the parent class, it returns true the first time, then false afterwards', () => { + + // Prepare + const utility = new Utility(); + const getColdStartSpy = jest.spyOn(utility, 'getColdStart'); + + // Act + utility.getColdStart(); + utility.getColdStart(); + utility.getColdStart(); + utility.getColdStart(); + utility.getColdStart(); + + // Assess + expect(getColdStartSpy).toHaveBeenCalledTimes(5); + expect(getColdStartSpy.mock.results).toEqual([ + expect.objectContaining({ value: true }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + ]); + + }); + + test('when called multiple times on a child class, it returns true the first time, then false afterwards', () => { + + // Prepare + class PowerTool extends Utility { + public constructor() { + super(); + } + + public dummyMethod(): boolean { + return this.getColdStart(); + } + } + const powertool = new PowerTool(); + const dummyMethodSpy = jest.spyOn(powertool, 'dummyMethod'); + const getColdStartSpy = jest.spyOn(powertool, 'getColdStart'); + + // Act + powertool.dummyMethod(); + powertool.dummyMethod(); + powertool.dummyMethod(); + powertool.dummyMethod(); + powertool.dummyMethod(); + + // Assess + expect(dummyMethodSpy).toHaveBeenCalledTimes(5); + expect(getColdStartSpy).toHaveBeenCalledTimes(5); + expect(dummyMethodSpy.mock.results).toEqual([ + expect.objectContaining({ value: true }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + ]); + + }); + + }); + + describe('Method: isColdStart', () => { + + test('when called multiple times on the parent class, it returns true the first time, then false afterwards', () => { + + // Prepare + const utility = new Utility(); + const isColdStartSpy = jest.spyOn(utility, 'isColdStart'); + + // Act + utility.isColdStart(); + utility.isColdStart(); + utility.isColdStart(); + utility.isColdStart(); + utility.isColdStart(); + + // Assess + expect(isColdStartSpy).toHaveBeenCalledTimes(5); + expect(isColdStartSpy.mock.results).toEqual([ + expect.objectContaining({ value: true }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + ]); + + }); + + test('when called multiple times on a child class, it returns true the first time, then false afterwards', () => { + + // Prepare + class PowerTool extends Utility { + public constructor() { + super(); + } + + public dummyMethod(): boolean { + return this.isColdStart(); + } + } + const powertool = new PowerTool(); + const dummyMethodSpy = jest.spyOn(powertool, 'dummyMethod'); + const isColdStartSpy = jest.spyOn(powertool, 'isColdStart'); + + // Act + powertool.dummyMethod(); + powertool.dummyMethod(); + powertool.dummyMethod(); + powertool.dummyMethod(); + powertool.dummyMethod(); + + // Assess + expect(dummyMethodSpy).toHaveBeenCalledTimes(5); + expect(isColdStartSpy).toHaveBeenCalledTimes(5); + expect(dummyMethodSpy.mock.results).toEqual([ + expect.objectContaining({ value: true }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + expect.objectContaining({ value: false }), + ]); + + }); + + }); + +}); \ No newline at end of file From 9c905b75025008d3f2e0e4efd4956c69a7a9c1c3 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 10 Feb 2022 11:37:47 +0100 Subject: [PATCH 2/5] feat: exported new Utility class --- packages/commons/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index b8269d62ea..1a056dbf93 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -1,3 +1,4 @@ export * from './utils/lambda'; +export * from './Utility'; export * as ContextExamples from './tests/resources/contexts'; export * as Events from './tests/resources/events'; \ No newline at end of file From 9088c7f66383da1f7b5d27bc444cc1ccd55f4b39 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Thu, 10 Feb 2022 11:38:41 +0100 Subject: [PATCH 3/5] chore: (housekeeping) added missing jest group comments to LambdaInterface tests --- packages/commons/tests/unit/LambdaInterface.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/commons/tests/unit/LambdaInterface.test.ts b/packages/commons/tests/unit/LambdaInterface.test.ts index 272961a913..4c0c8902af 100644 --- a/packages/commons/tests/unit/LambdaInterface.test.ts +++ b/packages/commons/tests/unit/LambdaInterface.test.ts @@ -1,3 +1,8 @@ +/** + * Test LambdaInterface interface + * + * @group unit/commons/lambdaInterface + */ import { Handler } from 'aws-lambda'; import { Callback, Context } from 'aws-lambda'; import { ContextExamples, SyncHandler, AsyncHandler, LambdaInterface } from '../../src'; @@ -102,6 +107,8 @@ describe('LambdaInterface with decorator', () => { class LambdaFunction implements LambdaInterface { @dummyModule.dummyDecorator() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore public async handler(_event: unknown, context: Context): Promise { context.getRemainingTimeInMillis(); @@ -118,6 +125,8 @@ describe('LambdaInterface with decorator', () => { class LambdaFunction implements LambdaInterface { @dummyModule.dummyDecorator() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore public handler(_event: unknown, context: Context, _callback: Callback): void { context.getRemainingTimeInMillis(); } From 40c6afd9ff624f2fb793bc11b1f5882a72beb412 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 11 Feb 2022 18:27:42 +0100 Subject: [PATCH 4/5] Update packages/commons/src/Utility.ts Co-authored-by: ijemmy --- packages/commons/src/Utility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commons/src/Utility.ts b/packages/commons/src/Utility.ts index 7a08ba698e..5bd33a539c 100644 --- a/packages/commons/src/Utility.ts +++ b/packages/commons/src/Utility.ts @@ -19,7 +19,7 @@ * * Emit a metric during a cold start function invocation in ``Metrics` * * Annotate the invocation segment with a `coldStart` key in `Tracer` * - * If instead you want use this logic in your own utilities, `Utility` provides two methdos: + * If you want to use this logic in your own utilities, `Utility` provides two methods: * * #### `getColdStart()` * From 25fd4a0c2f40ff5af5ee6ec08a40a08f3e2dc22d Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Fri, 11 Feb 2022 18:27:51 +0100 Subject: [PATCH 5/5] Update packages/commons/src/Utility.ts Co-authored-by: ijemmy --- packages/commons/src/Utility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/commons/src/Utility.ts b/packages/commons/src/Utility.ts index 5bd33a539c..9991f18cef 100644 --- a/packages/commons/src/Utility.ts +++ b/packages/commons/src/Utility.ts @@ -16,7 +16,7 @@ * * As a Powertools user you probably won't be using this class directly, in fact if you use other Powertools utilities the cold start heuristic found here is already used to: * * Add a `coldStart` key to the structured logs when injecting context information in `Logger` - * * Emit a metric during a cold start function invocation in ``Metrics` + * * Emit a metric during a cold start function invocation in `Metrics` * * Annotate the invocation segment with a `coldStart` key in `Tracer` * * If you want to use this logic in your own utilities, `Utility` provides two methods: