Skip to content

Commit 4e4091f

Browse files
dreamorosiijemmy
andauthored
feat(commons): centralize cold start heuristic (#547)
* 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 <[email protected]> * Update packages/commons/src/Utility.ts Co-authored-by: ijemmy <[email protected]> Co-authored-by: ijemmy <[email protected]>
1 parent 7c14ce7 commit 4e4091f

File tree

4 files changed

+231
-0
lines changed

4 files changed

+231
-0
lines changed

Diff for: packages/commons/src/Utility.ts

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* ## Intro
3+
* Utility is a base class that other Powertools utilites can extend to inherit shared logic.
4+
*
5+
*
6+
* ## Key features
7+
* * Cold Start heuristic to determine if the current
8+
*
9+
* ## Usage
10+
*
11+
* ### Cold Start
12+
*
13+
* 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.
14+
*
15+
* 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.
16+
*
17+
* 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:
18+
* * Add a `coldStart` key to the structured logs when injecting context information in `Logger`
19+
* * Emit a metric during a cold start function invocation in `Metrics`
20+
* * Annotate the invocation segment with a `coldStart` key in `Tracer`
21+
*
22+
* If you want to use this logic in your own utilities, `Utility` provides two methods:
23+
*
24+
* #### `getColdStart()`
25+
*
26+
* 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.
27+
*
28+
* @example
29+
* ```typescript
30+
* import { Utility } from '@aws-lambda-powertools/commons';
31+
*
32+
* const utility = new Utility();
33+
*
34+
* export const handler = async (_event: any, _context: any) => {
35+
* utility.getColdStart();
36+
* };
37+
* ```
38+
*
39+
* #### `isColdStart()`
40+
*
41+
* This method is an alias of `getColdStart()` and is exposed for convenience and better readability in certain usages.
42+
*
43+
* @example
44+
* ```typescript
45+
* import { Utility } from '@aws-lambda-powertools/commons';
46+
*
47+
* const utility = new Utility();
48+
*
49+
* export const handler = async (_event: any, _context: any) => {
50+
* if (utility.isColdStart()) {
51+
* // do something, this block is only executed on the first invocation of the function
52+
* } else {
53+
* // do something else, this block gets executed on all subsequent invocations
54+
* }
55+
* };
56+
* ```
57+
*/
58+
export class Utility {
59+
60+
private coldStart: boolean = true;
61+
62+
public getColdStart(): boolean {
63+
if (this.coldStart) {
64+
this.coldStart = false;
65+
66+
return true;
67+
}
68+
69+
return false;
70+
}
71+
72+
public isColdStart(): boolean {
73+
return this.getColdStart();
74+
}
75+
76+
}

Diff for: packages/commons/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './utils/lambda';
2+
export * from './Utility';
23
export * as ContextExamples from './tests/resources/contexts';
34
export * as Events from './tests/resources/events';

Diff for: packages/commons/tests/unit/LambdaInterface.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* Test LambdaInterface interface
3+
*
4+
* @group unit/commons/lambdaInterface
5+
*/
16
import { Handler } from 'aws-lambda';
27
import { Callback, Context } from 'aws-lambda';
38
import { ContextExamples, SyncHandler, AsyncHandler, LambdaInterface } from '../../src';
@@ -102,6 +107,8 @@ describe('LambdaInterface with decorator', () => {
102107
class LambdaFunction implements LambdaInterface {
103108

104109
@dummyModule.dummyDecorator()
110+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
111+
// @ts-ignore
105112
public async handler(_event: unknown, context: Context): Promise<unknown> {
106113
context.getRemainingTimeInMillis();
107114

@@ -118,6 +125,8 @@ describe('LambdaInterface with decorator', () => {
118125
class LambdaFunction implements LambdaInterface {
119126

120127
@dummyModule.dummyDecorator()
128+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
129+
// @ts-ignore
121130
public handler(_event: unknown, context: Context, _callback: Callback): void {
122131
context.getRemainingTimeInMillis();
123132
}

Diff for: packages/commons/tests/unit/Utility.test.ts

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/**
2+
* Test Utility class
3+
*
4+
* @group unit/commons/utility
5+
*/
6+
import { Utility } from '../../src';
7+
8+
describe('Class: Utility', () => {
9+
10+
beforeEach(() => {
11+
jest.clearAllMocks();
12+
jest.resetModules();
13+
});
14+
15+
describe('Method: getColdStart', () => {
16+
17+
test('when called multiple times on the parent class, it returns true the first time, then false afterwards', () => {
18+
19+
// Prepare
20+
const utility = new Utility();
21+
const getColdStartSpy = jest.spyOn(utility, 'getColdStart');
22+
23+
// Act
24+
utility.getColdStart();
25+
utility.getColdStart();
26+
utility.getColdStart();
27+
utility.getColdStart();
28+
utility.getColdStart();
29+
30+
// Assess
31+
expect(getColdStartSpy).toHaveBeenCalledTimes(5);
32+
expect(getColdStartSpy.mock.results).toEqual([
33+
expect.objectContaining({ value: true }),
34+
expect.objectContaining({ value: false }),
35+
expect.objectContaining({ value: false }),
36+
expect.objectContaining({ value: false }),
37+
expect.objectContaining({ value: false }),
38+
]);
39+
40+
});
41+
42+
test('when called multiple times on a child class, it returns true the first time, then false afterwards', () => {
43+
44+
// Prepare
45+
class PowerTool extends Utility {
46+
public constructor() {
47+
super();
48+
}
49+
50+
public dummyMethod(): boolean {
51+
return this.getColdStart();
52+
}
53+
}
54+
const powertool = new PowerTool();
55+
const dummyMethodSpy = jest.spyOn(powertool, 'dummyMethod');
56+
const getColdStartSpy = jest.spyOn(powertool, 'getColdStart');
57+
58+
// Act
59+
powertool.dummyMethod();
60+
powertool.dummyMethod();
61+
powertool.dummyMethod();
62+
powertool.dummyMethod();
63+
powertool.dummyMethod();
64+
65+
// Assess
66+
expect(dummyMethodSpy).toHaveBeenCalledTimes(5);
67+
expect(getColdStartSpy).toHaveBeenCalledTimes(5);
68+
expect(dummyMethodSpy.mock.results).toEqual([
69+
expect.objectContaining({ value: true }),
70+
expect.objectContaining({ value: false }),
71+
expect.objectContaining({ value: false }),
72+
expect.objectContaining({ value: false }),
73+
expect.objectContaining({ value: false }),
74+
]);
75+
76+
});
77+
78+
});
79+
80+
describe('Method: isColdStart', () => {
81+
82+
test('when called multiple times on the parent class, it returns true the first time, then false afterwards', () => {
83+
84+
// Prepare
85+
const utility = new Utility();
86+
const isColdStartSpy = jest.spyOn(utility, 'isColdStart');
87+
88+
// Act
89+
utility.isColdStart();
90+
utility.isColdStart();
91+
utility.isColdStart();
92+
utility.isColdStart();
93+
utility.isColdStart();
94+
95+
// Assess
96+
expect(isColdStartSpy).toHaveBeenCalledTimes(5);
97+
expect(isColdStartSpy.mock.results).toEqual([
98+
expect.objectContaining({ value: true }),
99+
expect.objectContaining({ value: false }),
100+
expect.objectContaining({ value: false }),
101+
expect.objectContaining({ value: false }),
102+
expect.objectContaining({ value: false }),
103+
]);
104+
105+
});
106+
107+
test('when called multiple times on a child class, it returns true the first time, then false afterwards', () => {
108+
109+
// Prepare
110+
class PowerTool extends Utility {
111+
public constructor() {
112+
super();
113+
}
114+
115+
public dummyMethod(): boolean {
116+
return this.isColdStart();
117+
}
118+
}
119+
const powertool = new PowerTool();
120+
const dummyMethodSpy = jest.spyOn(powertool, 'dummyMethod');
121+
const isColdStartSpy = jest.spyOn(powertool, 'isColdStart');
122+
123+
// Act
124+
powertool.dummyMethod();
125+
powertool.dummyMethod();
126+
powertool.dummyMethod();
127+
powertool.dummyMethod();
128+
powertool.dummyMethod();
129+
130+
// Assess
131+
expect(dummyMethodSpy).toHaveBeenCalledTimes(5);
132+
expect(isColdStartSpy).toHaveBeenCalledTimes(5);
133+
expect(dummyMethodSpy.mock.results).toEqual([
134+
expect.objectContaining({ value: true }),
135+
expect.objectContaining({ value: false }),
136+
expect.objectContaining({ value: false }),
137+
expect.objectContaining({ value: false }),
138+
expect.objectContaining({ value: false }),
139+
]);
140+
141+
});
142+
143+
});
144+
145+
});

0 commit comments

Comments
 (0)