fix: Avoid swap-file races when multiple plugin instances share a tadaOutputLocation#420
fix: Avoid swap-file races when multiple plugin instances share a tadaOutputLocation#420macrozone wants to merge 2 commits into
Conversation
…aOutputLocation
Multiple TS projects (e.g. in a monorepo extending a shared tsconfig)
can point their plugin instances at the same tadaOutputLocation. All
instances used the same swap-file path ('<output>.tmp'), so concurrent
regenerations raced: one instance renames the swap-file into place
while another still expects it, failing its rename and then throwing
ENOENT from the clean-up unlink — leaving the typings stale until a
manual regeneration.
The swap-file name is now unique per process and write, and clean-up
no longer masks the original rename error.
Fixes 0no-co/gql.tada#571
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 1358f0a The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
thx fable... |
| // If the file exists, we write to a swap-file, then rename (i.e. move) | ||
| // the file into place. No try-catch around `writeFile` for proper | ||
| // directory/permission errors | ||
| const tempTarget = target + '.tmp'; |
There was a problem hiding this comment.
This still races on the first write: if the output doesn't exist yet, concurrent instances all take this direct writeFile path. Can we use the unique swap+rename path for missing targets too?
There was a problem hiding this comment.
Good catch — done in 1358f0a. swapWrite now always writes to the unique swap-file and renames into place, for missing targets too; the direct-write branch is gone. Directory/permission errors still fall through since the swap-file lives in the same directory. Extended the unit test to run the 20-way concurrent write against both an existing and a missing target.
There was a problem hiding this comment.
(above was written by fable, just to clarify)
for me it looks fine, but i am testing it currently in my large repo to see whether the issue is resolved
Direct writes to a missing target could still interleave across concurrent plugin instances; the unique swap-file + atomic rename now covers first writes as well. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Fixes 0no-co/gql.tada#571.
When several TS projects configure the plugin with the same
tadaOutputLocation(e.g. a monorepo where ~10 tsconfigs extend one shared fragment), each plugin instance regenerates the same typings file.swapWriteused a single shared swap-file path (<output>.tmp) for all of them, so concurrent regenerations raced:<output>.d.ts.tmp.renamefails withENOENT, and its clean-upunlinkthrowsENOENTas well — which is the error that surfaces:After that, the in-editor tada types stay stale/broken until a manual CLI regeneration + TS server restart.
Fix
<output>.<pid>.<n>.tmp), covering both separate tsserver processes and multiple plugin instances within one process.renamestays atomic, so concurrent writers simply last-write-win without ever leaving a broken output.renameerror path is best-effort (.catch(() => {})), so a genuine rename failure is reported instead of being masked by the unlink error.Test
Added
test/unit/swapWrite.test.tswith a 20-way concurrent write test. Against the previous implementation it reproduces the exact error from the issue:With the fix, all writers resolve, the target contains one full write, and no
.tmpfiles are left behind.🤖 Generated with Claude Code