Repository navigation
Conversation
A synth removed the asset's whole folder, including the artifact for the current hash, so every synth reran every bundler. Only the folders of other hashes are removed now, and a bundled asset hashed before the build (SOURCE or CUSTOM) keeps its existing artifact instead of rebuilding. Staging writes to a sibling `<hash>.staging` folder that is renamed into place once it succeeds, so a failed or interrupted build never leaves a folder a later synth would reuse. Fixes open-constructs#582
jsteinich
left a comment
There was a problem hiding this comment.
Thanks, this is a clean fix for #582. Staging into a .staging sibling and renaming it into place is a good addition. A folder left over from an interrupted run is swept on the next synth, since its name isn't the hash. Two things before it lands:
1. Changing an asset's type can reuse an artifact of the wrong shape (packages/cdktn/src/terraform-asset.ts:293)
The hash doesn't depend on packaging. A bundled asset hashes the same as ARCHIVE and as DIRECTORY, with or without canonicalAssetHashes (checked locally). A directory packaging's targetPath is the hash folder itself, so existsSync(targetPath) is true whenever any artifact for that hash is there:
- Synth with
type: AssetType.ARCHIVEand a bundler:assets/asset/<H>/archive.zip. - Change to
type: AssetType.DIRECTORYand synth again: the hash is still<H>, andassets/asset/<H>/exists, so staging is skipped. - The directory asset now contains only
archive.zip, not the built tree, and nothing fails.
FILE → DIRECTORY hits the same problem. The other directions rebuild correctly, because they look for a file path that isn't there. On main this can't happen, because the whole folder is wiped each time. One option is to record the packaging (and file name) in a sidecar next to the hash folder, such as assets/<id>/<hash>.json. Keep it in the cleanup loop, and reuse only when it matches. Please add a test for the ARCHIVE → DIRECTORY case.
2. On Windows, renaming a directory fails with EPERM while any file inside it is open (terraform-asset.ts:310)
I checked this locally: with a handle open on a file inside <hash>.staging, fs.renameSync throws EPERM. Antivirus and the search indexer briefly open newly written files, which is why graceful-fs retries rename on Windows. This path now runs for every asset on every synth, unbundled ones included. Before, staging wrote in place and never renamed. Could the rename retry briefly on EPERM/EACCES/EBUSY (a few attempts with a short backoff)? Another option is to use the staging-and-rename path only for bundled assets, where it matters. Unbundled assets are always restaged anyway, so they gain nothing from it.
On the bundlerKey trade-off: I think it's acceptable. It matches the asset-pipeline design (#380: the build is deferred to onSynthesize so it can be skipped, and the hash is taken at construction). IAssetBundler.bundlerKey already documents that an incomplete key "silently reuses a stale artifact". LocalBundler and DockerBundler both build their key from their config. It is still a behaviour change users will notice in two cases:
- a hand-written bundler with no
bundlerKey, whose code or options change; - a build that reads inputs outside the asset
path: aLocalBundlerworkingDirectoryabove the source, a monorepo import, or a mutable Docker tag likenode:20.
These used to rebuild on every synth; now they keep the old artifact until cdktf.out is deleted. Please call it out in the release notes and the assets docs (the docs box is unchecked). Mention extraHash or deleting cdktf.out as the fix.
Not blocking: within one synth, two stacks with the same hash still build twice. Leaving that out is fine, as the PR says; the next synth reuses both.
The hash does not depend on the asset type, so an artifact staged as ARCHIVE or FILE was reused after the type changed to DIRECTORY, whose path is the hash folder itself. A `<hash>.json` file next to the hash folder now records the type and file name of the staged artifact. It is written once the staged folder is in place, and a synth reuses the artifact only when it matches. Only bundled SOURCE and CUSTOM assets, which can reuse their artifact, stage into `<hash>.staging` and rename it into place. The rename retries briefly on EPERM, EACCES and EBUSY, which Windows returns while antivirus or the indexer has a file in the folder open. Other assets are staged in place, as before. The `extraHash` and `bundler` docs now say when a bundled asset keeps its old artifact and how to force a rebuild.
|
Thanks for checking these locally. Pushed d31763d:
|
jsteinich
left a comment
There was a problem hiding this comment.
Thanks, both fixes look right.
- Packaging: the
<hash>.jsonsidecar closes the type-switch case. It is removed before any rebuild and written only after the rename, so a crash anywhere in between just rebuilds on the next synth. The two new→ DIRECTORYtests cover what I hit. - Windows rename: limiting the staging-and-rename path to bundled
SOURCE/CUSTOMassets puts unbundled assets back onmain's behaviour. I checked the retry against a real handle: another process held a file inside<hash>.stagingopen for 300ms, andrenameSyncWithRetrysucceeded after about 370ms. PlainrenameSyncfails withEPERMthere.
On release notes: please don't add a BREAKING CHANGE: footer. This follows the contract the bundlerKey docs already describe, so I'd rather not list it under breaking changes. The behaviour-change paragraph in the description is the right place for it. Could you add one more case there: a fixed assetHash (e.g. "v1") on a bundled asset now keeps its first artifact until the string changes. Before, it rebuilt on every synth.
eduardomourar
left a comment
There was a problem hiding this comment.
It would be nice to have those fixed before merging.
| // set when a bundler builds the artifact; names a single-file result | ||
| private readonly bundler?: IAssetBundler; | ||
| // true when the hash is known before the build, so a staged artifact can be reused | ||
| private readonly reusesStagedArtifact: boolean; |
There was a problem hiding this comment.
Stale artifact reuse — bundler swaps without bundlerKey silently serve old build output
There was a problem hiding this comment.
This is the trade-off jsteinich and I went through in the first review. The hash is taken at construction, and the only thing that identifies a bundler in it is its bundlerKey. Without one there's nothing stable to tell two bundlers apart: an object literal or a minified class has no reliable name. #582 asks for exactly this reuse (its repro bundler has no key), AWS CDK does the same, and IAssetBundler.bundlerKey already says an incomplete key reuses a stale artifact. LocalBundler, DockerBundler and ChainBundler all set one. Turning reuse off for keyless bundlers would bring back the rebuild-every-synth behaviour for the case in the issue, so I left the code as is. I did add the choice of bundler to the inputs the extraHash docs list, and the description calls this out as a behaviour change, with extraHash or deleting cdktf.out as the fix. Happy to revisit if you see it differently.
| ? stagingFolder | ||
| : path.join(stagingFolder, this.fileName), | ||
| ); | ||
| fs.rmSync(hashFolder, { recursive: true, force: true }); |
There was a problem hiding this comment.
Missing retry on cleanup rmSync (also on line 347) — inconsistent with the new renameSyncWithRetry, can cause spurious Windows failures.
There was a problem hiding this comment.
Done in 6b5ac3a. There's now an rmSyncWithRetry next to renameSyncWithRetry that uses Node's built-in maxRetries/retryDelay (it retries EBUSY/EPERM/ENOTEMPTY), with the same attempt count and base delay as the rename. It's used for the hash folder before the rename, for the staging-folder cleanup, and for the sweep of other hashes' folders. So a staging folder left behind by a failed cleanup is also removed with retries on the next synth.
| ); | ||
| fs.rmSync(hashFolder, { recursive: true, force: true }); | ||
| renameSyncWithRetry(stagingFolder, hashFolder); | ||
| } finally { |
There was a problem hiding this comment.
This finally cleanup can mask real build errors — a locked-file cleanup failure overwrites the original error.
There was a problem hiding this comment.
Good catch. The finally is now a catch that tries to remove the staging folder and then rethrows the original error. If that removal fails too, it's ignored: the next synth sweeps <hash>.staging anyway, since its name isn't the hash. On success the rename has already moved the folder, so there's nothing to clean up. New test: the build throws and removing the staging folder throws EBUSY; the synth reports the build error, and the next synth removes the leftover and rebuilds.
| const packaging = this.packaging; | ||
| const stagedPackaging = JSON.stringify({ | ||
| type: AssetType[this.type], | ||
| fileName: this.fileName, |
There was a problem hiding this comment.
Directory asset sidecar includes fileName — causes unnecessary rebuilds on rename/move despite unchanged hash.
There was a problem hiding this comment.
Right. For a directory the file name was just the source's basename and isn't part of path, so a renamed or moved source rebuilt for nothing. The sidecar now records fileName only for FILE and ARCHIVE. For those the name is part of the path anyway, so the ARCHIVE/FILE → DIRECTORY cases jsteinich raised still rebuild. New test: moving the source directory keeps the artifact.
Removing an asset's folders for other hashes, the hash folder before the rename, and the staging folder after a failure now retry on Windows file locks, like the rename. A cleanup that still fails no longer replaces the build error; the next synth sweeps the leftover staging folder. A directory artifact's `<hash>.json` no longer records a file name, which a directory has no use for, so moving its source no longer rebuilds it. The `extraHash` docs now name the choice of bundler as an input the hash cannot see.
Related issue
Fixes #582
Description
TerraformAsset._onSynthesizeremoved the asset's wholeassets/<id>/folder before staging, including the artifact for the current hash, so every synth reran every bundler. It now removes only the folders of other hashes, and a bundled asset whose hash is taken before the build (SOURCE, the default, orCUSTOM) keeps the artifact already staged for its hash instead of building it again. Unbundled assets are still re-copied, andOUTPUTis unchanged since it has already built in the constructor. As thebundlerKeydocs say, a bundler whose key doesn't capture its build config now reuses the old artifact when that config changes.This is a behaviour change in three cases, which used to rebuild on every synth and now keep the old artifact until
cdktf.outis deleted:bundlerKeywhose code or options change, or which is swapped for another bundler without a key;path(aLocalBundlerworkingDirectoryabove the source, a monorepo import, a mutable Docker tag likenode:20);assetHash(e.g."v1") on a bundled asset, which keeps its first artifact until the string changes.Set
extraHashto fold such inputs into the hash (or changeassetHashwhen it is set), or deletecdktf.out. TheextraHashandbundlerTSDoc now say this, and open-constructs/cdk-terrain-docs#41 adds a "Rebuilding Bundled Assets" section to the assets page.A bundled asset whose hash is known before the build stages into a sibling
<hash>.stagingfolder, which is renamed into place once it succeeds. The rename, and the removal of the old hash folder, the staging folder and other hashes' folders, retry briefly on the errors Windows returns while antivirus or the indexer has a file inside open. If the build fails, the original error is reported even when removing the staging folder fails too; the next synth sweeps the leftover. A failed or interrupted build therefore never leaves a folder a later synth would reuse. Since the hash doesn't depend on the asset type,assets/<id>/<hash>.jsonrecords the type of the staged artifact, and its file name forFILEandARCHIVE. It is written after the rename, and the artifact is reused only when it matches, so changingtype(e.g.ARCHIVE→DIRECTORY) rebuilds, while moving a directory asset's source does not. Unbundled andOUTPUTassets are staged in place as before. A folder left half-built by an older version would still be reused; deletingcdktf.outclears it.Building the same hash once across stacks within one synth (the third suggestion in the issue) is left out to keep this small; it could be a follow-up.
New tests in
canonical-asset-hash.test.tssynthesize twice into the same outdir with a bundler that counts its runs:typefromARCHIVEorFILEtoDIRECTORYrebuilds;EPERMis retried.All but the changed-hash case fail without this change.
On Windows,
matchers.test.ts,archive-symlink.test.tsandasset-hash.test.tsfail locally with and without this change (noterraformon the PATH, symlinks); everything else inpackages/cdktnpasses.Checklist