Skip to content

Commit 04adfca

Browse files
committed
fix: aspect in the middle
1 parent a67c3f5 commit 04adfca

File tree

4 files changed

+156
-22
lines changed

4 files changed

+156
-22
lines changed

packages/aws-cdk-lib/aws-s3/test/bucket.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import * as kms from '../../aws-kms';
66
import * as cdk from '../../core';
77
import * as s3 from '../lib';
88
import { ReplicationTimeValue } from '../lib/bucket';
9+
import { IConstruct } from 'constructs';
10+
import * as lambda from '../../aws-lambda';
11+
import * as lambdaev from '../../aws-lambda-event-sources';
912

1013
// to make it easy to copy & paste from output:
1114
/* eslint-disable quote-props */
@@ -4795,3 +4798,36 @@ describe('bucket', () => {
47954798
});
47964799
});
47974800
});
4801+
4802+
test('aspect prio test', () => {
4803+
class TagNameAspect implements cdk.IAspect {
4804+
public visit(node: IConstruct): void {
4805+
if (node instanceof s3.Bucket) {
4806+
cdk.Tags.of(node).add('Name', 'asdf', { priority: 10 });
4807+
}
4808+
}
4809+
}
4810+
const app = new cdk.App({
4811+
postCliContext: {
4812+
'@aws-cdk/core:aspectStabilization': true,
4813+
},
4814+
});
4815+
const stack = new cdk.Stack(app, 'stack');
4816+
var bucket = new s3.Bucket(stack, 'test-bucket', {
4817+
bucketName: 'test-bucket',
4818+
});
4819+
const fn = new lambda.Function(stack, 'test-function', {
4820+
functionName: 'test-function',
4821+
handler: 'handler',
4822+
runtime: lambda.Runtime.NODEJS_22_X,
4823+
code: lambda.Code.fromInline('function(){ }'),
4824+
});
4825+
4826+
fn.addEventSource(new lambdaev.S3EventSourceV2(bucket, {
4827+
events: [s3.EventType.OBJECT_CREATED],
4828+
}));
4829+
4830+
cdk.Aspects.of(stack).add(new TagNameAspect(), { priority: 10 });
4831+
4832+
app.synth();
4833+
});

packages/aws-cdk-lib/core/lib/aspect.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export class Aspects {
9090
return;
9191
}
9292
this._appliedAspects.push(newApplication);
93+
bumpAspectTreeRevision(this._scope);
9394
}
9495

9596
/**
@@ -109,6 +110,33 @@ export class Aspects {
109110
}
110111
}
111112

113+
/**
114+
* Bump the tree revision for the tree containing the given construct.
115+
*
116+
* Module-local function since I don't want to open up the door for *anyone* calling this.
117+
*/
118+
function bumpAspectTreeRevision(construct: IConstruct) {
119+
const root = construct.node.root;
120+
const rev = (root as any)[TREE_REVISION_SYM] ?? 0;
121+
(root as any)[TREE_REVISION_SYM] = rev + 1;
122+
}
123+
124+
/**
125+
* Return the aspect revision of the tree that the given construct is part of
126+
*
127+
* The revision number is changed every time an aspect is added to the tree.
128+
*
129+
* Instead of returning the version, returns a function that returns the
130+
* version: we can cache the root lookup inside the function; this saves
131+
* crawling the tree on every read, which is frequent.
132+
*
133+
* @internal
134+
*/
135+
export function _aspectTreeRevisionReader(construct: IConstruct): () => number {
136+
const root = construct.node.root;
137+
return () => (root as any)[TREE_REVISION_SYM] ?? 0;
138+
}
139+
112140
/**
113141
* Object respresenting an Aspect application. Stores the Aspect, the pointer to the construct it was applied
114142
* to, and the priority value of that Aspect.
@@ -155,3 +183,5 @@ export class AspectApplication {
155183
this._priority = priority;
156184
}
157185
}
186+
187+
const TREE_REVISION_SYM = Symbol.for('@aws-cdk/core.Aspects.treeRevision');

packages/aws-cdk-lib/core/lib/private/synthesis.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { CloudAssembly } from '../../../cx-api';
88
import * as cxapi from '../../../cx-api';
99
import { Annotations } from '../annotations';
1010
import { App } from '../app';
11-
import { AspectApplication, AspectPriority, Aspects } from '../aspect';
11+
import { _aspectTreeRevisionReader, AspectApplication, AspectPriority, Aspects } from '../aspect';
1212
import { FileSystem } from '../fs';
1313
import { Stack } from '../stack';
1414
import { ISynthesisSession } from '../stack-synthesizers/types';
@@ -37,7 +37,7 @@ export function synthesize(root: IConstruct, options: SynthesisOptions = { }): c
3737
// we start by calling "synth" on all nested assemblies (which will take care of all their children)
3838
synthNestedAssemblies(root, options);
3939

40-
if (options.aspectStabilization == true) {
40+
if (options.aspectStabilization) {
4141
invokeAspectsV2(root);
4242
} else {
4343
invokeAspects(root);
@@ -274,32 +274,33 @@ function invokeAspects(root: IConstruct) {
274274
* than the most recently invoked Aspect on that node.
275275
*/
276276
function invokeAspectsV2(root: IConstruct) {
277-
const invokedByPath: { [nodePath: string]: AspectApplication[] } = { };
277+
const invokedByPath = new Map<string, AspectApplication[]>();
278278

279279
recurse(root, []);
280280

281281
for (let i = 0; i <= 100; i++) {
282282
const didAnythingToTree = recurse(root, []);
283283

284-
if (!didAnythingToTree) {
284+
// Keep on invoking until nothing gets invoked anymore
285+
if (didAnythingToTree === 'nothing') {
285286
return;
286287
}
287288
}
288289

289290
throw new Error('We have detected a possible infinite loop while invoking Aspects. Please check your Aspects and verify there is no configuration that would cause infinite Aspect or Node creation.');
290291

291-
function recurse(construct: IConstruct, inheritedAspects: AspectApplication[]): boolean {
292+
function recurse(construct: IConstruct, inheritedAspects: AspectApplication[]): 'invoked' | 'aspect-added' | 'nothing' {
292293
const node = construct.node;
293294

294-
let didSomething = false;
295-
296-
let localAspects = getAspectApplications(construct);
297-
const allAspectsHere = sortAspectsByPriority(inheritedAspects, localAspects);
295+
let ret: ReturnType<typeof recurse> = 'nothing';
296+
const currentAspectTreeRevision = _aspectTreeRevisionReader(construct);
297+
const versionAtStart = currentAspectTreeRevision();
298298

299+
const allAspectsHere = sortAspectsByPriority(inheritedAspects, getAspectApplications(construct));
299300
for (const aspectApplication of allAspectsHere) {
300-
let invoked = invokedByPath[node.path];
301+
let invoked = invokedByPath.get(node.path);
301302
if (!invoked) {
302-
invoked = invokedByPath[node.path] = [];
303+
invokedByPath.set(node.path, invoked = []);
303304
}
304305

305306
if (invoked.some(invokedApp => invokedApp.aspect === aspectApplication.aspect)) {
@@ -310,25 +311,44 @@ function invokeAspectsV2(root: IConstruct) {
310311
const lastInvokedAspect = invoked[invoked.length - 1];
311312
if (lastInvokedAspect && lastInvokedAspect.priority > aspectApplication.priority) {
312313
throw new Error(
313-
`Cannot invoke Aspect ${aspectApplication.aspect.constructor.name} with priority ${aspectApplication.priority} on node ${node.path}: an Aspect ${lastInvokedAspect.aspect.constructor.name} with a lower priority (${lastInvokedAspect.priority}) was already invoked on this node.`,
314+
`Cannot invoke Aspect ${aspectApplication.aspect.constructor.name} with priority ${aspectApplication.priority} on node ${node.path}: an Aspect ${lastInvokedAspect.aspect.constructor.name} with a lower priority (added at ${lastInvokedAspect.construct.node.path} with priority ${lastInvokedAspect.priority}) was already invoked on this node.`,
314315
);
315316
}
316317

317318
aspectApplication.aspect.visit(construct);
318319

319-
didSomething = true;
320+
ret = 'invoked';
320321

321322
// mark as invoked for this node
322323
invoked.push(aspectApplication);
324+
325+
// If this aspect added another aspect, we need to reconsider everything;
326+
// it might have added an aspect above us and we need to restart the
327+
// entire tree. This could probably be made more efficient, but restarting
328+
// the tree from the top currently does it as well.
329+
if (currentAspectTreeRevision() !== versionAtStart) {
330+
return 'aspect-added';
331+
}
323332
}
324333

325-
let childDidSomething = false;
326334
for (const child of construct.node.children) {
327335
if (!Stage.isStage(child)) {
328-
childDidSomething = recurse(child, allAspectsHere) || childDidSomething;
336+
const childDidSomething = recurse(child, allAspectsHere);
337+
switch (childDidSomething) {
338+
case 'aspect-added':
339+
// Must abort immediately if we see this.
340+
return 'aspect-added';
341+
case 'invoked':
342+
// We have to keep on invoking until there's nothing left to do.
343+
ret = 'invoked';
344+
break;
345+
default:
346+
break;
347+
}
329348
}
330349
}
331-
return didSomething || childDidSomething;
350+
351+
return ret;
332352
}
333353
}
334354

packages/aws-cdk-lib/core/test/aspect.test.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Construct, IConstruct } from 'constructs';
2-
import { getWarnings } from './util';
32
import { Template } from '../../assertions';
43
import { Bucket, CfnBucket } from '../../aws-s3';
54
import * as cxschema from '../../cloud-assembly-schema';
@@ -298,7 +297,7 @@ describe('aspect', () => {
298297

299298
expect(() => {
300299
app.synth();
301-
}).toThrow('Cannot invoke Aspect Tag with priority 0 on node My-Stack: an Aspect Tag with a lower priority (10) was already invoked on this node.');
300+
}).toThrow('an Aspect Tag with a lower priority');
302301
});
303302

304303
test('Infinitely recursing Aspect is caught with error', () => {
@@ -338,14 +337,13 @@ describe('aspect', () => {
338337
Aspects.of(root).add(new Tag('AspectA', 'Visited'));
339338

340339
// "Monkey patching" - Override `applied` to simulate its absence
341-
Object.defineProperty(Aspects.prototype, 'applied', {
342-
value: undefined,
343-
configurable: true,
344-
});
340+
const appliedGetter = jest.spyOn(Aspects.prototype, 'applied', 'get').mockReturnValue(undefined as any);
345341

346342
expect(() => {
347343
app.synth();
348344
}).not.toThrow();
345+
346+
appliedGetter.mockRestore();
349347
});
350348

351349
test('RemovalPolicy: higher priority wins', () => {
@@ -456,4 +454,54 @@ describe('aspect', () => {
456454
DeletionPolicy: 'Retain',
457455
});
458456
});
457+
458+
test.each([
459+
'on self',
460+
'on parent',
461+
] as const)('aspect can insert another aspect %s in between other ones', (addWhere) => {
462+
// Test that between two pre-existing aspects with LOW and HIGH priorities,
463+
// the LOW aspect can add one in the middle, when stabilization is enabled.
464+
//
465+
// With stabilization (Aspects V2), we are stricter about the ordering and we would
466+
// throw if something doesn't work.
467+
468+
// GIVEN
469+
const app = new App();
470+
const root = new MyConstruct(app, 'Root');
471+
const aspectTarget = new MyConstruct(root, 'MyConstruct');
472+
473+
// WHEN
474+
// - Aspect with prio 100 adds an Aspect with prio 500
475+
// - An Aspect with prio 700 also exists
476+
// We should execute all and in the right order.
477+
const executed = new Array<string>();
478+
479+
class TestAspect implements IAspect {
480+
constructor(private readonly name: string, private readonly block?: () => void) {
481+
}
482+
483+
visit(node: IConstruct): void {
484+
// Only do something for the target node
485+
if (node.node.path === 'Root/MyConstruct') {
486+
executed.push(this.name);
487+
this.block?.();
488+
}
489+
}
490+
}
491+
492+
Aspects.of(aspectTarget).add(new TestAspect('one', () => {
493+
// We either add the new aspect on ourselves, or on an ancestor.
494+
//
495+
// In either case, it should execute next, before the 700 executes.
496+
const addHere = addWhere === 'on self' ? aspectTarget : root;
497+
498+
Aspects.of(addHere).add(new TestAspect('two'), { priority: 500 });
499+
}), { priority: 100 });
500+
Aspects.of(aspectTarget).add(new TestAspect('three'), { priority: 700 });
501+
502+
// THEN: should not throw and execute in the right order
503+
app.synth({ aspectStabilization: true });
504+
505+
expect(executed).toEqual(['one', 'two', 'three']);
506+
});
459507
});

0 commit comments

Comments
 (0)