Skip to content

Commit 0e08eeb

Browse files
authored
fix(cli): hotswap should wait for lambda's updateFunctionCode to complete (#18536)
There are [upcoming changes](https://aws.amazon.com/blogs/compute/coming-soon-expansion-of-aws-lambda-states-to-all-functions/) that will rollout Lambda states to all Lambda Functions. Prior to this update (current functionality) when you made an `updateFunctionCode` request the function was immediately available for both invocation and future updates. Once this change is rolled out this will no longer be the case. With Lambda states, when you make an update to a Lambda Function, it will not be available for future updates until the `LastUpdateStatus` returns `Successful`. This PR introduces a custom waiter that will wait for the update to complete before proceeding. The waiter will wait until the `State=Active` and the `LastUpdateStatus=Successful`. The `State` controls whether or not the function can be invoked, and the `LastUpdateStatus` controls whether the function can be updated. Based on this, I am not considering a deployment complete until both are successful. To see a more in depth analysis of the different values see #18386. In my testing I found that the time it took for a function to go from `LastUpdateStatus=InProgress` to `LastUpdateStatus=Successful` was: - ~1 second for a zip Function not in a VPC - ~25 seconds for a container Function or a Function in a VPC - ~2 minutes to deploy a VPC function (best proxy for StateReasonCode=Restoring) There are a couple of built in waiters that could have been used for this, namely [functionUpdated](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html#functionUpdated-waiter). This waiter uses `getFunctionConfiguration` which has a quota of 15 requests/second. In addition the waiter polls every 5 seconds and this cannot be configured. Because hotswapping is sensitive to any latency that is introduced, I created a custom waiter that uses `getFunction`. `getFunction` has a quota of 100 requests/second and the custom waiter can be configured to poll every 1 second or every 5 seconds depending on what type of function is being updated. fixes #18386 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 41c8a3f commit 0e08eeb

8 files changed

+323
-21
lines changed

packages/aws-cdk/lib/api/hotswap/lambda-functions.ts

+51-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Writable } from 'stream';
22
import * as archiver from 'archiver';
3+
import * as AWS from 'aws-sdk';
34
import { flatMap } from '../../util';
45
import { ISDK } from '../aws-auth';
56
import { CfnEvaluationException, EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
@@ -232,25 +233,18 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
232233
const operations: Promise<any>[] = [];
233234

234235
if (resource.code !== undefined) {
235-
const updateFunctionCodePromise = lambda.updateFunctionCode({
236+
const updateFunctionCodeResponse = await lambda.updateFunctionCode({
236237
FunctionName: this.lambdaFunctionResource.physicalName,
237238
S3Bucket: resource.code.s3Bucket,
238239
S3Key: resource.code.s3Key,
239240
ImageUri: resource.code.imageUri,
240241
ZipFile: resource.code.functionCodeZip,
241242
}).promise();
242243

244+
await this.waitForLambdasCodeUpdateToFinish(updateFunctionCodeResponse, lambda);
245+
243246
// only if the code changed is there any point in publishing a new Version
244247
if (this.lambdaFunctionResource.publishVersion) {
245-
// we need to wait for the code update to be done before publishing a new Version
246-
await updateFunctionCodePromise;
247-
// if we don't wait for the Function to finish updating,
248-
// we can get a "The operation cannot be performed at this time. An update is in progress for resource:"
249-
// error when publishing a new Version
250-
await lambda.waitFor('functionUpdated', {
251-
FunctionName: this.lambdaFunctionResource.physicalName,
252-
}).promise();
253-
254248
const publishVersionPromise = lambda.publishVersion({
255249
FunctionName: this.lambdaFunctionResource.physicalName,
256250
}).promise();
@@ -269,8 +263,6 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
269263
} else {
270264
operations.push(publishVersionPromise);
271265
}
272-
} else {
273-
operations.push(updateFunctionCodePromise);
274266
}
275267
}
276268

@@ -304,6 +296,53 @@ class LambdaFunctionHotswapOperation implements HotswapOperation {
304296
// run all of our updates in parallel
305297
return Promise.all(operations);
306298
}
299+
300+
/**
301+
* After a Lambda Function is updated, it cannot be updated again until the
302+
* `State=Active` and the `LastUpdateStatus=Successful`.
303+
*
304+
* Depending on the configuration of the Lambda Function this could happen relatively quickly
305+
* or very slowly. For example, Zip based functions _not_ in a VPC can take ~1 second whereas VPC
306+
* or Container functions can take ~25 seconds (and 'idle' VPC functions can take minutes).
307+
*/
308+
private async waitForLambdasCodeUpdateToFinish(currentFunctionConfiguration: AWS.Lambda.FunctionConfiguration, lambda: AWS.Lambda): Promise<void> {
309+
const functionIsInVpcOrUsesDockerForCode = currentFunctionConfiguration.VpcConfig?.VpcId ||
310+
currentFunctionConfiguration.PackageType === 'Image';
311+
312+
// if the function is deployed in a VPC or if it is a container image function
313+
// then the update will take much longer and we can wait longer between checks
314+
// otherwise, the update will be quick, so a 1-second delay is fine
315+
const delaySeconds = functionIsInVpcOrUsesDockerForCode ? 5 : 1;
316+
317+
// configure a custom waiter to wait for the function update to complete
318+
(lambda as any).api.waiters.updateFunctionCodeToFinish = {
319+
name: 'UpdateFunctionCodeToFinish',
320+
operation: 'getFunction',
321+
// equates to 1 minute for zip function not in a VPC and
322+
// 5 minutes for container functions or function in a VPC
323+
maxAttempts: 60,
324+
delay: delaySeconds,
325+
acceptors: [
326+
{
327+
matcher: 'path',
328+
argument: "Configuration.LastUpdateStatus == 'Successful' && Configuration.State == 'Active'",
329+
expected: true,
330+
state: 'success',
331+
},
332+
{
333+
matcher: 'path',
334+
argument: 'Configuration.LastUpdateStatus',
335+
expected: 'Failed',
336+
state: 'failure',
337+
},
338+
],
339+
};
340+
341+
const updateFunctionCodeWaiter = new (AWS as any).ResourceWaiter(lambda, 'updateFunctionCodeToFinish');
342+
await updateFunctionCodeWaiter.wait({
343+
FunctionName: this.lambdaFunctionResource.physicalName,
344+
}).promise();
345+
}
307346
}
308347

309348
/**

packages/aws-cdk/test/api/hotswap/hotswap-deployments.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ let mockGetEndpointSuffix: () => string;
88

99
beforeEach(() => {
1010
hotswapMockSdkProvider = setup.setupHotswapTests();
11-
mockUpdateLambdaCode = jest.fn();
11+
mockUpdateLambdaCode = jest.fn().mockReturnValue({});
1212
mockUpdateMachineDefinition = jest.fn();
1313
mockGetEndpointSuffix = jest.fn(() => 'amazonaws.com');
1414
hotswapMockSdkProvider.stubLambda({

packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,29 @@ export class HotswapMockSdkProvider {
8383
});
8484
}
8585

86-
public stubLambda(stubs: SyncHandlerSubsetOf<AWS.Lambda>) {
87-
this.mockSdkProvider.stubLambda(stubs);
86+
public stubLambda(
87+
stubs: SyncHandlerSubsetOf<AWS.Lambda>,
88+
serviceStubs?: SyncHandlerSubsetOf<AWS.Service>,
89+
additionalProperties: { [key: string]: any } = {},
90+
): void {
91+
this.mockSdkProvider.stubLambda(stubs, {
92+
api: {
93+
waiters: {},
94+
},
95+
makeRequest() {
96+
return {
97+
promise: () => Promise.resolve({}),
98+
response: {},
99+
addListeners: () => {},
100+
};
101+
},
102+
...serviceStubs,
103+
...additionalProperties,
104+
});
105+
}
106+
107+
public getLambdaApiWaiters(): { [key: string]: any } {
108+
return (this.mockSdkProvider.sdk.lambda() as any).api.waiters;
88109
}
89110

90111
public setUpdateProjectMock(mockUpdateProject: (input: codebuild.UpdateProjectInput) => codebuild.UpdateProjectOutput) {

packages/aws-cdk/test/api/hotswap/lambda-functions-docker-hotswap-deployments.test.ts

+61-1
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,26 @@ let mockUpdateLambdaCode: (params: Lambda.Types.UpdateFunctionCodeRequest) => La
55
let mockTagResource: (params: Lambda.Types.TagResourceRequest) => {};
66
let mockUntagResource: (params: Lambda.Types.UntagResourceRequest) => {};
77
let hotswapMockSdkProvider: setup.HotswapMockSdkProvider;
8+
let mockMakeRequest: (operation: string, params: any) => AWS.Request<any, AWS.AWSError>;
89

910
beforeEach(() => {
1011
hotswapMockSdkProvider = setup.setupHotswapTests();
11-
mockUpdateLambdaCode = jest.fn();
12+
mockUpdateLambdaCode = jest.fn().mockReturnValue({
13+
PackageType: 'Image',
14+
});
1215
mockTagResource = jest.fn();
1316
mockUntagResource = jest.fn();
17+
mockMakeRequest = jest.fn().mockReturnValue({
18+
promise: () => Promise.resolve({}),
19+
response: {},
20+
addListeners: () => {},
21+
});
1422
hotswapMockSdkProvider.stubLambda({
1523
updateFunctionCode: mockUpdateLambdaCode,
1624
tagResource: mockTagResource,
1725
untagResource: mockUntagResource,
26+
}, {
27+
makeRequest: mockMakeRequest,
1828
});
1929
});
2030

@@ -65,3 +75,53 @@ test('calls the updateLambdaCode() API when it receives only a code difference i
6575
ImageUri: 'new-image',
6676
});
6777
});
78+
79+
test('calls the getFunction() API with a delay of 5', async () => {
80+
// GIVEN
81+
setup.setCurrentCfnStackTemplate({
82+
Resources: {
83+
Func: {
84+
Type: 'AWS::Lambda::Function',
85+
Properties: {
86+
Code: {
87+
ImageUri: 'current-image',
88+
},
89+
FunctionName: 'my-function',
90+
},
91+
Metadata: {
92+
'aws:asset:path': 'old-path',
93+
},
94+
},
95+
},
96+
});
97+
const cdkStackArtifact = setup.cdkStackArtifactOf({
98+
template: {
99+
Resources: {
100+
Func: {
101+
Type: 'AWS::Lambda::Function',
102+
Properties: {
103+
Code: {
104+
ImageUri: 'new-image',
105+
},
106+
FunctionName: 'my-function',
107+
},
108+
Metadata: {
109+
'aws:asset:path': 'new-path',
110+
},
111+
},
112+
},
113+
},
114+
});
115+
116+
// WHEN
117+
await hotswapMockSdkProvider.tryHotswapDeployment(cdkStackArtifact);
118+
119+
// THEN
120+
expect(mockMakeRequest).toHaveBeenCalledWith('getFunction', { FunctionName: 'my-function' });
121+
expect(hotswapMockSdkProvider.getLambdaApiWaiters()).toEqual(expect.objectContaining({
122+
updateFunctionCodeToFinish: expect.objectContaining({
123+
name: 'UpdateFunctionCodeToFinish',
124+
delay: 5,
125+
}),
126+
}));
127+
});

0 commit comments

Comments
 (0)