Repository navigation
fix(rules): readers no longer wait on a cache refresh - #394
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
⚡ Grok Review
Reviewed commit: 27dc60b991
❌ Patch is incorrect: 1 finding.
The WAL switch leaves the rules cache on SQLite's default locking, so a refresh still blocks readers and the Windows timeout failure remains.
Enable WAL locking so refreshes stop blocking readers
crates/devkit-rules/src/cache.rs#L88-L96
RulesCache::open still uses rusqlite's default rollback-journal locking. begin_write takes a RESERVED lock that blocks readers for the whole refresh, so concurrent_refreshes_leave_one_whole_index can still fail with database is locked after the 2 s busy timeout, and hooks can fail the same way. PRAGMA journal_mode=WAL only changes the journal; it does not switch the connection to WAL locking. Call Connection::pragma_update(None, "journal_mode", "WAL") or execute_batch("PRAGMA journal_mode=WAL;") before the schema statements so readers see the last committed index while a refresh writes.
let conn = Connection::open(&path).context("opening the rules cache")?;
conn.pragma_update(None, "journal_mode", "WAL")
.context("switching the rules cache to WAL")?;
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY,
body TEXT NOT NULL
);",
)
.context("creating the rules cache schema")?;
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27dc60b991
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Superseded by the review of f95e1bf
A reader blocks behind a refresh holding the cache's write lock and fails with "database is locked" once the busy timeout runs out. Back-to-back refreshes starved a reader that way on a Windows runner. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The non-blocking launch test failed once on Windows with no record of what the server did. Its log says whether Python failed to bind or never started. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Under the rollback journal a refresh's write lock blocks every reader, so back-to-back refreshes starved a hook's read past the busy timeout and it failed with "database is locked". In WAL mode a reader reads the last committed index while a refresh runs, which keeps the old-or-new guarantee without the wait. The cache is devkit's own file in its state directory, so WAL's extra files are fine there. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
f95e1bf to
ae229ed
Compare
There was a problem hiding this comment.
⚡ Grok Review
Reviewed commit: ae229eddd3
❌ Patch is incorrect: 1 finding.
The rules cache now writes WAL while every hook still opens it read-only, which cannot use a WAL file. That breaks every cached read after the first refresh.
Open the rules cache read-write so WAL reads work
crates/devkit-rules/src/cache.rs#L184-L187
After the first refresh, write sets journal_mode=WAL and leaves -wal/-shm beside the cache. Every hook and command then calls RuleCache::read and meta, which open the file with SQLITE_OPEN_READ_ONLY. SQLite cannot open a WAL database that way: it must create or write the -shm file, so the open fails with a disk I/O error ("unable to open database file") or SQLITE_CANTOPEN. CachedSource::read returns that error, so a write hook injects no rules and devkit rules query/stats/context fail even though a committed index is on disk. The new test never hits this path: it opens a second read-write connection, which can read WAL. Open the cache read-write (or SQLITE_OPEN_READWRITE plus SQLITE_OPEN_URI with mode=ro is not enough), then set PRAGMA query_only=ON so a reader still cannot change rows. On Windows, also give the directory write permission and do not delete -wal/-shm while a reader is open.
let conn = Connection::open_with_flags(
&self.path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
.with_context(|| self.reading())?;
conn.pragma_update(None, "query_only", "ON")
.with_context(|| self.reading())?;
|
On the Grok review of 27dc60b: that was the test-only commit, before WAL was switched on. The suggested code names a |
False positive. Since SQLite 3.22, a read-only connection opens a WAL database whenever it can create the -shm file, and the state directory is writable. RuleCache::read and meta open read-only after a WAL write in round_trips_the_index, a_refresh_in_progress_does_not_hold_up_a_reader and the cached_source tests, and all of them pass on ubuntu, macOS and Windows in run 38936335255.
🤖 I have created a release *beep* *boop* --- ## [0.14.23](v0.14.22...v0.14.23) (2026-10-16) ### Features * **pr:** check repro items red to green ([#383](#383)) ([2df9708](2df9708)) * **pr:** flag weakened tests between base and head ([#358](#358)) ([370a637](370a637)) * **pr:** require a passing verify run at HEAD ([#375](#375)) ([e557d03](e557d03)) * **tasks:** record every devrun task run ([#357](#357)) ([c64ebff](c64ebff)) * **ticket:** check ticket fields on MCP writes and discover them ([#377](#377)) ([d0e753d](d0e753d)) * **ticket:** create and edit Linear tickets ([#376](#376)) ([37a0289](37a0289)) * **ticket:** declare ticket fields and write them on GitHub ([#366](#366)) ([8f74838](8f74838)) * **todo:** install a todo shim for devkit todo ([#353](#353)) ([d9dfb2f](d9dfb2f)) * **todo:** name todos readably in messages ([#391](#391)) ([0cb1e29](0cb1e29)) * **todo:** promote a todo to a ticket ([#388](#388)) ([8f961ae](8f961ae)) * **todo:** push ticket parents and blockers ([#384](#384)) ([a14d187](a14d187)) * **todo:** push ticket status changes to the tracker ([#381](#381)) ([06fdaf2](06fdaf2)) * **todo:** refresh tickets and move them on todo changes ([#367](#367)) ([c6e7566](c6e7566)) * **todo:** resolve ticket keys and word names as ids ([#389](#389)) ([e161dfb](e161dfb)) * **todo:** show readable todo names in listings ([#390](#390)) ([362aad5](362aad5)) * **todo:** sync GitHub tickets into the todo store ([#359](#359)) ([8a897af](8a897af)) * **tracker:** sync Linear tickets into the todo store ([#369](#369)) ([916bad4](916bad4)) ### Bug Fixes * **deps:** bump rustls to 0.23.46 ([#361](#361)) ([4bf0dc0](4bf0dc0)) * **git:** rebuild fixture templates the CI cache emptied ([#396](#396)) ([577479f](577479f)) * **rules:** readers no longer wait on a cache refresh ([#394](#394)) ([5d9ca92](5d9ca92)) * windows probe, console and stdio defects slowing CI ([#393](#393)) ([2ba4653](2ba4653)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Agent summary
Problem
On the release PR, the rules cache test
concurrent_refreshes_leave_one_whole_indexfailed on Windows withdatabase is locked. The cache uses SQLite's rollback journal, so a refresh's write lock blocks every reader. Two refreshes back to back kept a reader waiting longer than its 2 s busy timeout. Hooks read this cache, so a hook can fail the same way while refreshes pile up.In the same partition,
launch_non_blocking_returns_before_readiness_then_status_flipsfailed with "server never started listening" and left no record of why.Change
a_refresh_in_progress_does_not_hold_up_a_readerholds a refresh open and reads through it.Proof
🤖 Generated with Claude Opus 5.5 via Claude Code