fix(deno): stop replacing cache databases that are still in use - #747
Merged
Merged
Conversation
When a cache database fails to initialize twice, `open_connection` deletes the file and recreates it. Contention between connections in the same process (`SQLITE_BUSY`, including after a `SQLITE_CANTOPEN` while the parent directory is being created) reaches that path. The recreated file has a new inode, so SQLite treats the existing `-shm` file as unused and truncates it while the other connections still have it mapped, and their next wal-index access raises SIGBUS. Retry lock contention (`SQLITE_BUSY`, `SQLITE_LOCKED`) with bounded exponential backoff on every attempt, and delete the file only for `SQLITE_CORRUPT` and `SQLITE_NOTADB`. Any other failure, including a path that still cannot be opened once its parent directory exists, falls back to the configured failure mode without deleting anything. The shape follows denoland/deno#34873, which only classifies the first error. Refs #746 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`caches()` built a new `Caches` on every call, so one factory opened several connections to the same dependency and node analysis databases. Keep it in a `Deferred` like the other factory accessors. Refs #746 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
johnstonmatt
approved these changes
Sep 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #746.
What is wrong
The process can die with SIGBUS (exit 135) while user workers boot at the same time on a fresh
DENO_DIR.EmitterFactory::caches()builds a newCacheson every call, and each one opensdep_analysis_cache_v2andnode_analysis_cache_v2. The main service and every user worker have their own factory. The result is several connections initializing the same two SQLite files at once, in the same process.SQLITE_BUSY, or withSQLITE_CANTOPENwhile the parent directory is still being created. After two failures,open_connectiondeletes the database and creates it again.-shmfile as unused. POSIX locks held by the same process are invisible toF_GETLK, so SQLite truncates the file. Connections that still have the old wal-index mapped then raise SIGBUS on their next access past the first page.denoland/deno#34873 stops the deletion on lock contention, but it only checks the first error, so a
CANTOPENfollowed byBUSYstill deletes the file.Changes
deno/cache/cache_db.rs: every failed attempt is classified, not just the first.BUSYandLOCKEDare retried with bounded exponential backoff (about 1.3 s in total). The file is deleted only forSQLITE_CORRUPTandSQLITE_NOTADB. Other failures fall back to the configuredon_failuremode as before. That includes a path that still can't be opened after its parent directory is created: it falls back immediately, without deleting anything. The helpers follow fix(cache): retry locked cache database instead of deleting it denoland/deno#34873 (retry_open_with_backoff,log_failure_mode,handle_failure_mode), and the init functions returnrusqlite::Erroras they do upstream, which should make future syncs easier.crates/deno_facade/emitter.rs:caches()is memoized in aDeferred, like the other factory accessors, so each factory opens each cache once.The vendored
cache_db.rsgets a test module with regression tests:CANTOPENfollowed byBUSYdoes not delete the file;Without the fix, the four contention and open-error tests fail every time. The concurrent test is timing-dependent: it failed in 4 of 30 runs, for example with
SQLITE_IOERR_SHORT_READ("file truncated?").Notes
SQLITE_BUSYwithout calling the busy handler, so the fix retries with a new connection and leaves the busy timeout as it is.CachesperDENO_DIRacross the process would remove that case too, and could be done as a follow-up.🤖 Generated with Claude Code