You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Cloud Assemblies are supposed to be read-locked as they are being used,
so that concurrent writers into the same directories do not trample on
the `cdk.out` directory as we are deploying it.
This is being done with the class `RWLock` which represents a read/write
lock. A lock is associated to a directory, which can have either at most
one writer or multiple readers.
This PR is a synthesis of #335 and #339.
Closesaws/aws-cdk#32964, closes#335, closes#339.
# Design
Because we need to add the concept of a lock to a Cloud Assembly, and we
want that resource to be disposable, we introduce the class
`IReadableCloudAssembly`. This interface models both a cloud assembly
and a dispose function which releases the lock and optionally cleans up
the backing directory of the cloud assembly.
Cloud Assembly Sources (`ICloudAssemblySource.produce()`) now returns a
`IReadableCloudAssembly` with a lock associated. The factory functions
start off by acquiring write locks on the backing directory during
synthesis, which are converted to read locks after a successful
synthesis.
* All toolkit functions take an `ICloudAssemblySource`, and dispose of
the produced `IReadableCloudAssembly` after use;
* Except `synth()` now returns a `CachedCloudAssembly`. That class
implements both `IReadableCloudAssembly` (as in, it is a cloud assembly
that is locked and ready to be read), as well as `ICloudAssemblySource`
so that it can be passed into other toolkit functions. The
`CachedCloudAssembly.produce()` returns borrowed versions of
`IReadableCloudAssembly` with dispose functions that don't do anything:
only the top-level dispose actually cleans anything up. This is so that
if the result of `synth()` is passed into (multiple) other toolkit
functions, their dispose calls don't accidentally release the lock. This
could have been done with reference counting, but is currently being
done by giving out a "borrowed" Cloud Assembly which doesn't clean up at
all.
* The locking itself is managed by `ExecutionEnvironment`; this is now a
disposable object which will hold a lock that either gets cleaned up
automatically with the object, or get converted to a reader lock when
the `ExecutionEnvironment` is explicitly marked as having successfully
completed. That same change now allows us to automatically clean up
temporary directories if synthesis fails, or when the CloudAssembly in
them is disposed of.
# Supporting changes
Supporting changes in this PR, necessary to make the above work:
- `StackAssembly`, which is a helper class to query a Cloud Assembly,
used to be an `ICloudAssemblySource` but it no longer is. That
functionality isn't used anywhere.
- Rename `CachedCloudAssemblySource` to `CachedCloudAssembly` and make
`synth()` return this concretely. It makes more sense to think of this
class as a Cloud Assembly (that happens to be usable as a source), than
the reverse.
- Locks can now be released more than once; calls after the first won't
do anything. This is to prevent an `_unlock()` followed by a `dispose()`
from doing something unexpected, and it also shouldn't fail.
- Rename `ILock` => `IReadLock` to contrast it more clearly with an
`IWriteLock`.
- Remove `IdentityCloudAssemblySource`, since it fills much the same
role as `CachedCloudAssembly`.
- `ExecutionEnvironment` is now disposable, and its sync constructor has
been changed to an async factory function.
- A lot of tests that called `cx.produce()` directory now have to use an
`await using` statement to make sure the read locks are cleaned up,
otherwise they get added to the GitHub repo.
# New tests for new contracts
- Cloud Assemblies are properly disposed by all toolkit methods that
take a CloudAssemblySource
- The return value of `toolkit.synth()` can be passed to other toolkit
methods, but its contents are only disposed when the object is
explicitly disposed, not when it's passed to toolkit methods.
- When Cloud Assembly producers fail, they should leave the directory
unlocked if given, or no directory at all if they are temporary.
# Breaking change
BREAKING CHANGE: this change changes the return type of
`toolkit.synth()`: it no longer returns an arbitrary Assembly Source (by
interface), but a specific Assembly, by class, that can also be used as
a source. The return type of `ICloudAssemblySource.produce()` has been
changed to `IReadableCloudAssembly`. This will only affect consumers
with custom implementations of that interface, the factory function APIs
are unchanged.
---
By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache-2.0 license
Copy file name to clipboardExpand all lines: packages/@aws-cdk/tmp-toolkit-helpers/src/api/rwlock.ts
+34-12
Original file line number
Diff line number
Diff line change
@@ -29,19 +29,24 @@ export class RWLock {
29
29
*
30
30
* No other readers or writers must exist for the given directory.
31
31
*/
32
-
publicasyncacquireWrite(): Promise<IWriterLock>{
32
+
publicasyncacquireWrite(): Promise<IWriteLock>{
33
33
awaitthis.assertNoOtherWriters();
34
34
35
-
constreaders=awaitthis.currentReaders();
35
+
constreaders=awaitthis._currentReaders();
36
36
if(readers.length>0){
37
37
thrownewToolkitError(`Other CLIs (PID=${readers}) are currently reading from ${this.directory}. Invoke the CLI in sequence, or use '--output' to synth into different directories.`);
// Releasing needs a flag, otherwise we might delete a file that some other lock has created in the mean time.
46
+
if(!released){
47
+
awaitdeleteFile(this.writerFile);
48
+
released=true;
49
+
}
45
50
},
46
51
convertToReaderLock: async()=>{
47
52
// Acquire the read lock before releasing the write lock. Slightly less
@@ -58,7 +63,7 @@ export class RWLock {
58
63
*
59
64
* Will fail if there are any writers.
60
65
*/
61
-
publicasyncacquireRead(): Promise<ILock>{
66
+
publicasyncacquireRead(): Promise<IReadLock>{
62
67
awaitthis.assertNoOtherWriters();
63
68
returnthis.doAcquireRead();
64
69
}
@@ -77,27 +82,37 @@ export class RWLock {
77
82
/**
78
83
* Do the actual acquiring of a read lock.
79
84
*/
80
-
privateasyncdoAcquireRead(): Promise<ILock>{
85
+
privateasyncdoAcquireRead(): Promise<IReadLock>{
81
86
constreaderFile=this.readerFile();
82
87
awaitwriteFileAtomic(readerFile,this.pidString);
88
+
89
+
letreleased=false;
83
90
return{
84
91
release: async()=>{
85
-
awaitdeleteFile(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
+
awaitdeleteFile(readerFile);
95
+
released=true;
96
+
}
86
97
},
87
98
};
88
99
}
89
100
90
101
privateasyncassertNoOtherWriters(){
91
-
constwriter=awaitthis.currentWriter();
102
+
constwriter=awaitthis._currentWriter();
92
103
if(writer){
93
104
thrownewToolkitError(`Another CLI (PID=${writer}) is currently synthing to ${this.directory}. Invoke the CLI in sequence, or use '--output' to synth into different directories.`);
94
105
}
95
106
}
96
107
97
108
/**
98
109
* Check the current writer (if any)
110
+
*
111
+
* Publicly accessible for testing purposes. Do not use.
0 commit comments