Skip to content

Commit a8bc46d

Browse files
authored
fix(cli): excessive stack event polling during deployment (#32196)
Closes #32186 ### Reason for this change <!--What is the bug or use case behind this change?--> The CLI will poll for all stack events from the beginning of time, even though it will end up using only a fraction of them to display in the terminal. This can cause a significant slowdown of `cdk deploy`. ### Description of changes <!--What code changes did you make? Have you made any important design decisions?--> Moved the pagination to the caller side, so it can decide whether or not to poll the next page. This is how it was before `2.167.0` https://github.com/aws/aws-cdk/blob/7bb9203eb95fe894c0d40942ff49c782a9fec251/packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts#L73-L74 ### Description of how you validated changes <!--Have you added any unit tests and/or integration tests?--> Added unit test. ### Notes - There are several functions in the [sdk.ts](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/aws-auth/sdk.ts) that perform implicit pagination to retrieve all results. From a quick glance, none of them seem to be misusing it though (at least with respect to how it always was). I will investigate those functions further in a followup PR. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 6317a2a commit a8bc46d

File tree

3 files changed

+136
-59
lines changed

3 files changed

+136
-59
lines changed

packages/aws-cdk/lib/api/aws-auth/sdk.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ import {
5151
DescribeResourceScanCommand,
5252
type DescribeResourceScanCommandInput,
5353
type DescribeResourceScanCommandOutput,
54+
DescribeStackEventsCommand,
5455
type DescribeStackEventsCommandInput,
56+
DescribeStackEventsCommandOutput,
5557
DescribeStackResourcesCommand,
5658
DescribeStackResourcesCommandInput,
5759
DescribeStackResourcesCommandOutput,
@@ -86,12 +88,10 @@ import {
8688
ListStacksCommand,
8789
ListStacksCommandInput,
8890
ListStacksCommandOutput,
89-
paginateDescribeStackEvents,
9091
paginateListStackResources,
9192
RollbackStackCommand,
9293
RollbackStackCommandInput,
9394
RollbackStackCommandOutput,
94-
StackEvent,
9595
StackResourceSummary,
9696
StartResourceScanCommand,
9797
type StartResourceScanCommandInput,
@@ -404,7 +404,7 @@ export interface ICloudFormationClient {
404404
input: UpdateTerminationProtectionCommandInput,
405405
): Promise<UpdateTerminationProtectionCommandOutput>;
406406
// Pagination functions
407-
describeStackEvents(input: DescribeStackEventsCommandInput): Promise<StackEvent[]>;
407+
describeStackEvents(input: DescribeStackEventsCommandInput): Promise<DescribeStackEventsCommandOutput>;
408408
listStackResources(input: ListStackResourcesCommandInput): Promise<StackResourceSummary[]>;
409409
}
410410

@@ -664,13 +664,8 @@ export class SDK {
664664
input: UpdateTerminationProtectionCommandInput,
665665
): Promise<UpdateTerminationProtectionCommandOutput> =>
666666
client.send(new UpdateTerminationProtectionCommand(input)),
667-
describeStackEvents: async (input: DescribeStackEventsCommandInput): Promise<StackEvent[]> => {
668-
const stackEvents = Array<StackEvent>();
669-
const paginator = paginateDescribeStackEvents({ client }, input);
670-
for await (const page of paginator) {
671-
stackEvents.push(...(page?.StackEvents || []));
672-
}
673-
return stackEvents;
667+
describeStackEvents: (input: DescribeStackEventsCommandInput): Promise<DescribeStackEventsCommandOutput> => {
668+
return client.send(new DescribeStackEventsCommand(input));
674669
},
675670
listStackResources: async (input: ListStackResourcesCommandInput): Promise<StackResourceSummary[]> => {
676671
const stackResources = Array<StackResourceSummary>();

packages/aws-cdk/lib/api/util/cloudformation/stack-event-poller.ts

+48-49
Original file line numberDiff line numberDiff line change
@@ -88,65 +88,64 @@ export class StackEventPoller {
8888
private async doPoll(): Promise<ResourceEvent[]> {
8989
const events: ResourceEvent[] = [];
9090
try {
91-
const eventList = await this.cfn.describeStackEvents({
92-
StackName: this.props.stackName,
93-
});
94-
for (const event of eventList) {
95-
// Event from before we were interested in 'em
96-
if (this.props.startTime !== undefined && event.Timestamp!.valueOf() < this.props.startTime) {
97-
return events;
98-
}
99-
100-
// Already seen this one
101-
if (this.eventIds.has(event.EventId!)) {
102-
return events;
103-
}
104-
this.eventIds.add(event.EventId!);
105-
106-
// The events for the stack itself are also included next to events about resources; we can test for them in this way.
107-
const isParentStackEvent = event.PhysicalResourceId === event.StackId;
108-
109-
if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) {
110-
return events;
91+
let nextToken: string | undefined;
92+
let finished = false;
93+
94+
while (!finished) {
95+
const page = await this.cfn.describeStackEvents({ StackName: this.props.stackName, NextToken: nextToken });
96+
for (const event of page?.StackEvents ?? []) {
97+
// Event from before we were interested in 'em
98+
if (this.props.startTime !== undefined && event.Timestamp!.valueOf() < this.props.startTime) {
99+
return events;
100+
}
101+
102+
// Already seen this one
103+
if (this.eventIds.has(event.EventId!)) {
104+
return events;
105+
}
106+
this.eventIds.add(event.EventId!);
107+
108+
// The events for the stack itself are also included next to events about resources; we can test for them in this way.
109+
const isParentStackEvent = event.PhysicalResourceId === event.StackId;
110+
111+
if (isParentStackEvent && this.props.stackStatuses?.includes(event.ResourceStatus ?? '')) {
112+
return events;
113+
}
114+
115+
// Fresh event
116+
const resEvent: ResourceEvent = {
117+
event: event,
118+
parentStackLogicalIds: this.props.parentStackLogicalIds ?? [],
119+
isStackEvent: isParentStackEvent,
120+
};
121+
events.push(resEvent);
122+
123+
if (
124+
!isParentStackEvent &&
125+
event.ResourceType === 'AWS::CloudFormation::Stack' &&
126+
isStackBeginOperationState(event.ResourceStatus)
127+
) {
128+
// If the event is not for `this` stack and has a physical resource Id, recursively call for events in the nested stack
129+
this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']);
130+
}
131+
132+
if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) {
133+
this.complete = true;
134+
}
111135
}
112136

113-
// Fresh event
114-
const resEvent: ResourceEvent = {
115-
event: event,
116-
parentStackLogicalIds: this.props.parentStackLogicalIds ?? [],
117-
isStackEvent: isParentStackEvent,
118-
};
119-
events.push(resEvent);
120-
121-
if (
122-
!isParentStackEvent &&
123-
event.ResourceType === 'AWS::CloudFormation::Stack' &&
124-
isStackBeginOperationState(event.ResourceStatus)
125-
) {
126-
// If the event is not for `this` stack and has a physical resource Id, recursively call for events in the nested stack
127-
this.trackNestedStack(event, [...(this.props.parentStackLogicalIds ?? []), event.LogicalResourceId ?? '']);
137+
nextToken = page?.NextToken;
138+
if (nextToken === undefined) {
139+
finished = true;
128140
}
129141

130-
if (isParentStackEvent && isStackTerminalState(event.ResourceStatus)) {
131-
this.complete = true;
132-
}
133142
}
134143
} catch (e: any) {
135144
if (!(e.name === 'ValidationError' && e.message === `Stack [${this.props.stackName}] does not exist`)) {
136145
throw e;
137146
}
138147
}
139-
// // Also poll all nested stacks we're currently tracking
140-
// for (const [logicalId, poller] of Object.entries(this.nestedStackPollers)) {
141-
// events.push(...(await poller.poll()));
142-
// if (poller.complete) {
143-
// delete this.nestedStackPollers[logicalId];
144-
// }
145-
// }
146-
147-
// // Return what we have so far
148-
// events.sort((a, b) => a.event.Timestamp!.valueOf() - b.event.Timestamp!.valueOf());
149-
// this.events.push(...events);
148+
150149
return events;
151150
}
152151

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { DescribeStackEventsCommand, DescribeStackEventsCommandInput, StackEvent } from '@aws-sdk/client-cloudformation';
2+
import { StackEventPoller } from '../../../../lib/api/util/cloudformation/stack-event-poller';
3+
import { MockSdk, mockCloudFormationClient } from '../../../util/mock-sdk';
4+
5+
beforeEach(() => {
6+
jest.resetAllMocks();
7+
});
8+
9+
describe('poll', () => {
10+
11+
test('polls all necessary pages', async () => {
12+
13+
const deployTime = Date.now();
14+
15+
const postDeployEvent1: StackEvent = {
16+
Timestamp: new Date(deployTime + 1000),
17+
EventId: 'event-1',
18+
StackId: 'stack-id',
19+
StackName: 'stack',
20+
};
21+
22+
const postDeployEvent2: StackEvent = {
23+
Timestamp: new Date(deployTime + 2000),
24+
EventId: 'event-2',
25+
StackId: 'stack-id',
26+
StackName: 'stack',
27+
};
28+
29+
const sdk = new MockSdk();
30+
mockCloudFormationClient.on(DescribeStackEventsCommand).callsFake((input: DescribeStackEventsCommandInput) => {
31+
const result = {
32+
StackEvents: input.NextToken === 'token' ? [postDeployEvent2] : [postDeployEvent1],
33+
NextToken: input.NextToken === 'token' ? undefined : 'token', // simulate a two page event stream.
34+
};
35+
36+
return result;
37+
});
38+
39+
const poller = new StackEventPoller(sdk.cloudFormation(), {
40+
stackName: 'stack',
41+
startTime: new Date().getTime(),
42+
});
43+
44+
const events = await poller.poll();
45+
expect(events.length).toEqual(2);
46+
47+
});
48+
49+
test('does not poll unnecessary pages', async () => {
50+
51+
const deployTime = Date.now();
52+
53+
const preDeployTimeEvent: StackEvent = {
54+
Timestamp: new Date(deployTime - 1000),
55+
EventId: 'event-1',
56+
StackId: 'stack-id',
57+
StackName: 'stack',
58+
};
59+
60+
const sdk = new MockSdk();
61+
mockCloudFormationClient.on(DescribeStackEventsCommand).callsFake((input: DescribeStackEventsCommandInput) => {
62+
63+
// the first event we return should stop the polling. we therefore
64+
// do not expect a second page to be polled.
65+
expect(input.NextToken).toBe(undefined);
66+
67+
return {
68+
StackEvents: [preDeployTimeEvent],
69+
NextToken: input.NextToken === 'token' ? undefined : 'token', // simulate a two page event stream.
70+
};
71+
72+
});
73+
74+
const poller = new StackEventPoller(sdk.cloudFormation(), {
75+
stackName: 'stack',
76+
startTime: new Date().getTime(),
77+
});
78+
79+
await poller.poll();
80+
81+
});
82+
83+
});

0 commit comments

Comments
 (0)