Skip to content

Commit

Permalink
feat(commons): centralize cold start heuristic (#547)
Browse files Browse the repository at this point in the history
* feat: added Utility common class w/ cold start heuristic + tests

* feat: exported new Utility class

* chore: (housekeeping) added missing jest group comments to LambdaInterface tests

* Update packages/commons/src/Utility.ts

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

* Update packages/commons/src/Utility.ts

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

Co-authored-by: ijemmy <ijemmy@users.noreply.github.com>
  • Loading branch information
dreamorosi and ijemmy authored Feb 16, 2022
1 parent 7c14ce7 commit 4e4091f
Show file tree
Hide file tree
Showing 4 changed files with 231 additions and 0 deletions.
76 changes: 76 additions & 0 deletions packages/commons/src/Utility.ts
Original file line number Diff line number Diff line change
@@ -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 you want to use this logic in your own utilities, `Utility` provides two methods:
*
* #### `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();
}

}
1 change: 1 addition & 0 deletions packages/commons/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions packages/commons/tests/unit/LambdaInterface.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<unknown> {
context.getRemainingTimeInMillis();

Expand All @@ -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();
}
Expand Down
145 changes: 145 additions & 0 deletions packages/commons/tests/unit/Utility.test.ts
Original file line number Diff line number Diff line change
@@ -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 }),
]);

});

});

});

0 comments on commit 4e4091f

Please sign in to comment.