Skip to content

Commit be679e7

Browse files
committed
Introduce IReadableCloudAssembly
4 parents 60a6a13 + e0df58f + 46395b3 + 6a413c1 commit be679e7

File tree

21 files changed

+388
-132
lines changed

21 files changed

+388
-132
lines changed

packages/@aws-cdk/tmp-toolkit-helpers/src/api/rwlock.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class RWLock {
2929
*
3030
* No other readers or writers must exist for the given directory.
3131
*/
32-
public async acquireWrite(): Promise<IWriterLock> {
32+
public async acquireWrite(): Promise<IWriteLock> {
3333
await this.assertNoOtherWriters();
3434

3535
const readers = await this.currentReaders();
@@ -39,9 +39,14 @@ export class RWLock {
3939

4040
await writeFileAtomic(this.writerFile, this.pidString);
4141

42+
let released = false;
4243
return {
4344
release: async () => {
44-
await deleteFile(this.writerFile);
45+
// Releasing needs a flag, otherwise we might delete a file that some other lock has created in the mean time.
46+
if (!released) {
47+
await deleteFile(this.writerFile);
48+
released = true;
49+
}
4550
},
4651
convertToReaderLock: async () => {
4752
// Acquire the read lock before releasing the write lock. Slightly less
@@ -58,7 +63,7 @@ export class RWLock {
5863
*
5964
* Will fail if there are any writers.
6065
*/
61-
public async acquireRead(): Promise<ILock> {
66+
public async acquireRead(): Promise<IReadLock> {
6267
await this.assertNoOtherWriters();
6368
return this.doAcquireRead();
6469
}
@@ -77,12 +82,18 @@ export class RWLock {
7782
/**
7883
* Do the actual acquiring of a read lock.
7984
*/
80-
private async doAcquireRead(): Promise<ILock> {
85+
private async doAcquireRead(): Promise<IReadLock> {
8186
const readerFile = this.readerFile();
8287
await writeFileAtomic(readerFile, this.pidString);
88+
89+
let released = false;
8390
return {
8491
release: async () => {
85-
await deleteFile(readerFile);
92+
// Releasing needs a flag, otherwise we might delete a file that some other lock has created in the mean time.
93+
if (!released) {
94+
await deleteFile(readerFile);
95+
released = true;
96+
}
8697
},
8798
};
8899
}
@@ -151,18 +162,21 @@ export class RWLock {
151162
/**
152163
* An acquired lock
153164
*/
154-
export interface ILock {
165+
export interface IReadLock {
166+
/**
167+
* Release the lock. Can be called more than once.
168+
*/
155169
release(): Promise<void>;
156170
}
157171

158172
/**
159173
* An acquired writer lock
160174
*/
161-
export interface IWriterLock extends ILock {
175+
export interface IWriteLock extends IReadLock {
162176
/**
163177
* Convert the writer lock to a reader lock
164178
*/
165-
convertToReaderLock(): Promise<ILock>;
179+
convertToReaderLock(): Promise<IReadLock>;
166180
}
167181

168182
/* c8 ignore start */ // code paths are unpredictable

packages/@aws-cdk/toolkit-lib/lib/actions/bootstrap/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class BootstrapEnvironments {
2525
static fromCloudAssemblySource(cx: ICloudAssemblySource): BootstrapEnvironments {
2626
return new BootstrapEnvironments(async (ioHost: IIoHost) => {
2727
const ioHelper = asIoHelper(ioHost, 'bootstrap');
28-
const assembly = await assemblyFromSource(ioHelper, cx);
28+
await using assembly = await assemblyFromSource(ioHelper, cx);
2929
const stackCollection = await assembly.selectStacksV2(ALL_STACKS);
3030
return stackCollection.stackArtifacts.map(stack => stack.environment);
3131
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type * as cxapi from '@aws-cdk/cx-api';
2+
import { BorrowedAssembly } from './private/borrowed-assembly';
3+
import type { ICloudAssemblySource, IReadableCloudAssembly } from './types';
4+
5+
/**
6+
* A CloudAssemblySource that is caching its result once produced.
7+
*
8+
* Most Toolkit interactions should use a cached source. Not caching is
9+
* relevant when the source changes frequently and it is to expensive to predict
10+
* if the source has changed.
11+
*
12+
* The `CachedCloudAssembly` is both itself a readable CloudAssembly, as well as
13+
* a Cloud Assembly Source. The lifetimes of cloud assemblies produced by this
14+
* source are coupled to the lifetime of the `CachedCloudAssembly`. In other
15+
* words: the `dispose()` functions of those cloud assemblies don't do anything;
16+
* only the `dispose()` function of the `CachedCloudAssembly` will be used.
17+
*
18+
* FIXME: We should consider referencing counting here, although that seems
19+
* unnecessarily complicated for now. Be aware to callers that failing to dispose
20+
* the result if a `produce()` call of a `CachedCloudAssembly` is considered
21+
* a bug and may lead to resource leakage in the future, even though it
22+
* might work today.
23+
*/
24+
export class CachedCloudAssembly implements ICloudAssemblySource, IReadableCloudAssembly {
25+
private asm: IReadableCloudAssembly;
26+
27+
public constructor(asm: IReadableCloudAssembly) {
28+
this.asm = asm;
29+
}
30+
31+
public get cloudAssembly(): cxapi.CloudAssembly {
32+
return this.asm.cloudAssembly;
33+
}
34+
35+
public async produce(): Promise<IReadableCloudAssembly> {
36+
return new BorrowedAssembly(this.asm.cloudAssembly);
37+
}
38+
39+
public _unlock() {
40+
return this.asm._unlock();
41+
}
42+
43+
public dispose(): Promise<void> {
44+
return this.asm.dispose();
45+
}
46+
47+
public [Symbol.asyncDispose](): Promise<void> {
48+
return this.dispose();
49+
}
50+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { StackSelectionStrategy, StackSelector } from '../../api/shared-public';
2+
export * from './cached-source';
23
export * from './source-builder';
34
export * from './types';
45

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type * as cxapi from '@aws-cdk/cx-api';
2+
import type { IReadableCloudAssembly } from '../types';
3+
4+
/**
5+
* An implementation of `IReadableCloudAssembly` that does nothing except hold on to the CloudAssembly object
6+
*
7+
* It does not own a lock, and it does not clean the underlying directory.
8+
*/
9+
export class BorrowedAssembly implements IReadableCloudAssembly {
10+
constructor(public readonly cloudAssembly: cxapi.CloudAssembly) {
11+
}
12+
13+
public async _unlock(): Promise<void> {
14+
}
15+
16+
public async dispose(): Promise<void> {
17+
}
18+
19+
public async [Symbol.asyncDispose](): Promise<void> {
20+
}
21+
}
22+

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/cached-source.ts

Lines changed: 0 additions & 25 deletions
This file was deleted.

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { MissingContext } from '@aws-cdk/cloud-assembly-schema';
2-
import type * as cxapi from '@aws-cdk/cx-api';
32
import type { ToolkitServices } from '../../../toolkit/private';
43
import { IO } from '../../io/private';
54
import { contextproviders } from '../../shared-private';
65
import { PROJECT_CONTEXT, type Context, type IoHelper } from '../../shared-private';
76
import { ToolkitError } from '../../shared-public';
8-
import type { ICloudAssemblySource } from '../types';
7+
import type { ICloudAssemblySource, IReadableCloudAssembly } from '../types';
98

109
export interface ContextAwareCloudAssemblyProps {
1110
/**
@@ -37,9 +36,22 @@ export interface ContextAwareCloudAssemblyProps {
3736
}
3837

3938
/**
40-
* Represent the Cloud Executable and the synthesis we can do on it
39+
* A CloudAssemblySource that wraps another CloudAssemblySource and runs a lookup loop on it
40+
*
41+
* This means that if the underlying CloudAssemblySource produces a manifest
42+
* with provider queries in it, the `ContextAwareCloudAssemblySource` will
43+
* perform the necessary context lookups and invoke the underlying
44+
* `CloudAssemblySource` again with thew missing context information.
45+
*
46+
* This is only useful if the underlying `CloudAssemblySource` can respond to
47+
* this new context information (it must be a CDK app source); if it is just a
48+
* static directory,
49+
*
50+
* The context is passed between `ContextAwareCloudAssemblySource` and the wrapped
51+
* cloud assembly source via a contex file on disk, so the wrapped assembly source
52+
* should re-read the context file on every invocation.
4153
*/
42-
export class ContextAwareCloudAssembly implements ICloudAssemblySource {
54+
export class ContextAwareCloudAssemblySource implements ICloudAssemblySource {
4355
private canLookup: boolean;
4456
private context: Context;
4557
private contextFile: string;
@@ -55,15 +67,16 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource {
5567
/**
5668
* Produce a Cloud Assembly, i.e. a set of stacks
5769
*/
58-
public async produce(): Promise<cxapi.CloudAssembly> {
70+
public async produce(): Promise<IReadableCloudAssembly> {
5971
// We may need to run the cloud assembly source multiple times in order to satisfy all missing context
6072
// (When the source producer runs, it will tell us about context it wants to use
6173
// but it missing. We'll then look up the context and run the executable again, and
6274
// again, until it doesn't complain anymore or we've stopped making progress).
6375
let previouslyMissingKeys: Set<string> | undefined;
6476
while (true) {
65-
const assembly = await this.source.produce();
77+
const readableAsm = await this.source.produce();
6678

79+
const assembly = readableAsm.cloudAssembly;
6780
if (assembly.manifest.missing && assembly.manifest.missing.length > 0) {
6881
const missingKeysSet = missingContextKeys(assembly.manifest.missing);
6982
const missingKeys = Array.from(missingKeysSet);
@@ -100,11 +113,12 @@ export class ContextAwareCloudAssembly implements ICloudAssemblySource {
100113
await this.context.save(this.contextFile);
101114

102115
// Execute again
116+
// TODO: Unlock
103117
continue;
104118
}
105119
}
106120

107-
return assembly;
121+
return readableAsm;
108122
}
109123
}
110124
}

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/identity-source.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from './context-aware-source';
2-
export * from './cached-source';
32
export * from './identity-source';
43
export * from './stack-assembly';
54
export * from './exec';

packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema';
55
import * as cxapi from '@aws-cdk/cx-api';
66
import * as fs from 'fs-extra';
77
import { lte } from 'semver';
8-
import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, guessExecutable } from '../../../api/shared-private';
98
import { type SdkProvider, type IoHelper, loadTree, some, Settings } from '../../../api/shared-private';
9+
import { prepareDefaultEnvironment as oldPrepare, prepareContext, spaceAvailableForContext, guessExecutable } from '../../../api/shared-private';
1010
import { splitBySize, versionNumber } from '../../../private/util';
1111
import type { ToolkitServices } from '../../../toolkit/private';
1212
import { IO } from '../../io/private';
@@ -17,6 +17,8 @@ type Env = { [key: string]: string };
1717
type Context = { [key: string]: any };
1818

1919
export class ExecutionEnvironment {
20+
public readonly outDirIsTemporary: boolean;
21+
2022
private readonly ioHelper: IoHelper;
2123
private readonly sdkProvider: SdkProvider;
2224
private readonly debugFn: (msg: string) => Promise<void>;
@@ -27,6 +29,7 @@ export class ExecutionEnvironment {
2729
this.sdkProvider = services.sdkProvider;
2830
this.debugFn = (msg: string) => this.ioHelper.notify(IO.DEFAULT_ASSEMBLY_DEBUG.msg(msg));
2931
this._outdir = props.outdir;
32+
this.outDirIsTemporary = props.outdir === undefined;
3033
}
3134

3235
/**

0 commit comments

Comments
 (0)