Skip to content

Commit 8fcb0e2

Browse files
TheRealAmazonKendraBrennan Ho
authored and
Brennan Ho
committed
fix(custom-resources): inactive lambda functions fail on invoke (aws#22612)
closes aws#20123 All lambda functions can become inactive eventually. This will result in invocations failing. This PR adds logic to wait for functions to become active on a failed invocation. ---- ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 319be7b commit 8fcb0e2

File tree

44 files changed

+634
-219
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+634
-219
lines changed

packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/outbound.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,31 @@ async function defaultInvokeFunction(req: AWS.Lambda.InvocationRequest): Promise
4343
lambda = new AWS.Lambda(awsSdkConfig);
4444
}
4545

46-
return lambda.invoke(req).promise();
46+
try {
47+
/**
48+
* Try an initial invoke.
49+
*
50+
* When you try to invoke a function that is inactive, the invocation fails and Lambda sets
51+
* the function to pending state until the function resources are recreated.
52+
* If Lambda fails to recreate the resources, the function is set to the inactive state.
53+
*
54+
* We're using invoke first because `waitFor` doesn't trigger an inactive function to do anything,
55+
* it just runs `getFunction` and checks the state.
56+
*/
57+
return await lambda.invoke(req).promise();
58+
} catch (error) {
59+
60+
/**
61+
* The status of the Lambda function is checked every second for up to 300 seconds.
62+
* Exits the loop on 'Active' state and throws an error on 'Inactive' or 'Failed'.
63+
*
64+
* And now we wait.
65+
*/
66+
await lambda.waitFor('functionActiveV2', {
67+
FunctionName: req.FunctionName,
68+
}).promise();
69+
return await lambda.invoke(req).promise();
70+
}
4771
}
4872

4973
export let startExecution = defaultStartExecution;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import * as aws from 'aws-sdk';
2+
import { invokeFunction } from '../../lib/provider-framework/runtime/outbound';
3+
4+
jest.mock('aws-sdk', () => {
5+
return {
6+
Lambda: class {
7+
public invoke() {
8+
return { promise: () => mockInvoke() };
9+
}
10+
11+
public waitFor() {
12+
return { promise: () => mockWaitFor() };
13+
}
14+
},
15+
};
16+
});
17+
18+
let mockInvoke: () => Promise<aws.Lambda.InvocationResponse>;
19+
20+
const req: aws.Lambda.InvocationRequest = {
21+
FunctionName: 'Whatever',
22+
Payload: {
23+
IsThisATest: 'Yes, this is a test',
24+
AreYouSure: 'Yes, I am sure',
25+
},
26+
};
27+
28+
let invokeCount: number = 0;
29+
let expectedFunctionStates: string[] = [];
30+
let receivedFunctionStates: string[] = [];
31+
32+
const mockWaitFor = async (): Promise<aws.Lambda.GetFunctionResponse> => {
33+
let state = expectedFunctionStates.pop();
34+
while (state !== 'Active') {
35+
receivedFunctionStates.push(state!);
36+
// If it goes back to inactive it's failed
37+
if (state === 'Inactive') throw new Error('Not today');
38+
// If failed... it's failed
39+
if (state === 'Failed') throw new Error('Broken');
40+
// If pending, continue the loop, no other valid options
41+
if (state !== 'Pending') throw new Error('State is confused');
42+
state = expectedFunctionStates.pop();
43+
}
44+
receivedFunctionStates.push(state);
45+
return {
46+
Configuration: {
47+
State: 'Active',
48+
},
49+
};
50+
};
51+
52+
describe('invokeFunction tests', () => {
53+
afterEach(() => {
54+
invokeCount = 0;
55+
expectedFunctionStates = [];
56+
receivedFunctionStates = [];
57+
});
58+
59+
// Success cases
60+
test('Inactive function that reactivates does not throw error', async () => {
61+
mockInvoke = async () => {
62+
if (invokeCount == 0) {
63+
invokeCount++;
64+
throw new Error('Better luck next time');
65+
}
66+
invokeCount++;
67+
return { Payload: req.Payload };
68+
};
69+
70+
expectedFunctionStates.push('Active');
71+
expectedFunctionStates.push('Pending');
72+
73+
expect(await invokeFunction(req)).toEqual({ Payload: req.Payload });
74+
expect(invokeCount).toEqual(2);
75+
expect(receivedFunctionStates).toEqual(['Pending', 'Active']);
76+
});
77+
78+
test('Active function does not run waitFor or retry invoke', async () => {
79+
mockInvoke = async () => {
80+
if (invokeCount == 1) {
81+
invokeCount++;
82+
throw new Error('This should not happen in this test');
83+
}
84+
invokeCount++;
85+
return { Payload: req.Payload };
86+
};
87+
88+
expectedFunctionStates.push('Active');
89+
90+
expect(await invokeFunction(req)).toEqual({ Payload: req.Payload });
91+
expect(invokeCount).toEqual(1);
92+
expect(receivedFunctionStates).toEqual([]);
93+
});
94+
95+
// Failure cases
96+
test('Inactive function that goes back to inactive throws error', async () => {
97+
mockInvoke = async () => {
98+
if (invokeCount == 0) {
99+
invokeCount++;
100+
throw new Error('Better luck next time');
101+
}
102+
invokeCount++;
103+
return { Payload: req.Payload };
104+
};
105+
106+
expectedFunctionStates.push('Inactive');
107+
expectedFunctionStates.push('Pending');
108+
expectedFunctionStates.push('Pending');
109+
110+
await expect(invokeFunction(req)).rejects.toThrowError(new Error('Not today'));
111+
expect(invokeCount).toEqual(1);
112+
expect(receivedFunctionStates).toEqual(['Pending', 'Pending', 'Inactive']);
113+
});
114+
115+
test('Inactive function that goes to failed throws error', async () => {
116+
mockInvoke = async () => {
117+
if (invokeCount == 0) {
118+
invokeCount++;
119+
throw new Error('Better luck next time');
120+
}
121+
invokeCount++;
122+
return { Payload: req.Payload };
123+
};
124+
125+
expectedFunctionStates.push('Failed');
126+
expectedFunctionStates.push('Pending');
127+
expectedFunctionStates.push('Pending');
128+
129+
await expect(invokeFunction(req)).rejects.toThrowError(new Error('Broken'));
130+
expect(invokeCount).toEqual(1);
131+
expect(receivedFunctionStates).toEqual(['Pending', 'Pending', 'Failed']);
132+
});
133+
134+
test('Inactive function that returns other value throws error', async () => {
135+
mockInvoke = async () => {
136+
if (invokeCount == 0) {
137+
invokeCount++;
138+
throw new Error('Better luck next time');
139+
}
140+
invokeCount++;
141+
return { Payload: req.Payload };
142+
};
143+
144+
expectedFunctionStates.push('NewFunctionWhoDis');
145+
expectedFunctionStates.push('Pending');
146+
expectedFunctionStates.push('Pending');
147+
148+
await expect(invokeFunction(req)).rejects.toThrowError(new Error('State is confused'));
149+
expect(invokeCount).toEqual(1);
150+
expect(receivedFunctionStates).toEqual(['Pending', 'Pending', 'NewFunctionWhoDis']);
151+
});
152+
153+
test('Wait for stops on terminal responses', async () => {
154+
mockInvoke = async () => {
155+
if (invokeCount == 0) {
156+
invokeCount++;
157+
throw new Error('Better luck next time');
158+
}
159+
invokeCount++;
160+
return { Payload: req.Payload };
161+
};
162+
163+
expectedFunctionStates.push('SomethingElse');
164+
expectedFunctionStates.push('Pending');
165+
expectedFunctionStates.push('Inactive');
166+
expectedFunctionStates.push('Pending');
167+
expectedFunctionStates.push('Pending');
168+
169+
await expect(invokeFunction(req)).rejects.toThrowError(new Error('Not today'));
170+
expect(invokeCount).toEqual(1);
171+
expect(receivedFunctionStates).toEqual(['Pending', 'Pending', 'Inactive']);
172+
});
173+
});
174+

packages/@aws-cdk/lambda-layer-awscli/test/integ.awscli-layer.js.snapshot/asset.7215c88dd3e638d28329d4538b36cdbfb54233a4d972181795814f8b904d1037/outbound.js

-45
This file was deleted.

0 commit comments

Comments
 (0)