Repository navigation
Package mirror: never advertise a package before its complete file is in place - #327
Conversation
The crawler wrote each mirror file with fs.writeFileSync on its final name, which truncates and rewrites in place. nginx serves the mirror directory, so a download during a write, most likely when a direct copy replaces a Simplifier copy, could get a partial tarball. With a CDN in front of /web/ that partial file would be cached for everyone. Write beside the target and rename over it, so readers see either the old file or the complete new one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This one makes my skin crawl, honestly. For the following reasons:
the last cuts both ways, I guess. But this change just feels super-under done to me |
|
Thanks — fair questions. Short answers, since production here is Linux:
Happy to add the win32 fallback, or to close this if you'd rather not take it. |
The crawler committed a package to the database (which starts the download redirect to the mirror) and only then wrote its mirror file, so every newly published package briefly had a missing or partly written file behind an advertised id#version. Now the file is written beside its target first; a new package's file is renamed into place before the commit, and a replacement's after it, so the old file is served until the replacement commits. A failed commit leaves the served file alone and removes a new package's file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Updated, and you were right that it wasn't robust enough, though not because of the rename itself. The crawler commits a package to the database before writing its mirror file. So for every new package, from any feed and not just the Simplifier case, there was a moment when the registry advertised it but the file was missing or half-written. Write-then-rename alone only turned the partial file into a brief 404. The new commit fixes the order:
So there's no moment at which an advertised package lacks a complete file. A test for the new-package case fails on current main. The description is rewritten to match. |
Every package the crawler stores, from any feed, can briefly be served as a missing or partial tarball. This PR makes sure an advertised
id#versiononly ever has a complete file behind it.The problem
In
packages/package-crawler.jsthe crawler:bucketPath) to<id>-<version>.tgzin the mirror, which nginx serves at/web/and the CDN fetches on a miss;fs.writeFileSyncunder its final name, which truncates and rewrites in place.That gives two windows:
The change
The new tarball is written beside its target as
<file>.tmp-<pid>, which never ends in.tgzand so is never served. It is then renamed onto the real name, and the step order depends on the case:On Linux,
renamewithin one directory is atomic, and a reader that already has the old file open keeps reading it to the end. So readers see either the old file or the complete new one, never a mixture.Tests
New cases in
tests/packages/package-crawler-versions.test.js:All 176 tests in
tests/packagespass.🤖 Generated with Claude Code