Skip to content

fix(rules): readers no longer wait on a cache refresh - #394

Merged
AbysmalBiscuit merged 3 commits into
mainfrom
flaky-windows-tests
Oct 16, 2026
Merged

AbysmalBiscuit merged 3 commits into
mainfrom
flaky-windows-tests

Conversation

@AbysmalBiscuit

Copy link
Copy Markdown
Owner

Agent summary

Problem

On the release PR, the rules cache test concurrent_refreshes_leave_one_whole_index failed on Windows with database 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_flips failed with "server never started listening" and left no record of why.

Change

  • The cache switches to WAL journaling, so a reader reads the last committed index and never waits on a refresh. a_refresh_in_progress_does_not_hold_up_a_reader holds a refresh open and reads through it.
  • The launch test prints the server's log when it fails, so the next failure shows whether Python failed to bind or never started.

Proof

  1. test a_refresh_in_progress_does_not_hold_up_a_reader, failing on the test-only commit and passing with WAL
  2. test launch_non_blocking_returns_before_readiness_then_status_flips prints the log in its failure message

🤖 Generated with Claude Opus 5.5 via Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T16:23:47.563431Z 27dc60b PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ 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.

P1 Badge 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")?;

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread crates/devkit-rules/tests/cache.rs
@github-actions
github-actions Bot dismissed their stale review October 16, 2026 16:32

Superseded by the review of f95e1bf

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ Grok Review

Reviewed commit: f95e1bfd5a

✅ No issues found.

The WAL switch and the launch-test log both leave existing callers and tests working: readers still see one committed index, and the assertion only adds the server log to a failure message.

AbysmalBiscuit and others added 3 commits October 16, 2026 19:00
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>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚡ 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.

P0 Badge 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())?;

@AbysmalBiscuit

Copy link
Copy Markdown
Owner Author

On the Grok review of 27dc60b: that was the test-only commit, before WAL was switched on. The suggested code names a RulesCache::open that does not exist. Setting journal_mode to WAL is itself what moves SQLite to WAL locking, and the fix commit (now ae229ed) does that in RuleCache::write. Grok's review of the fix commit reported no issues, and the review was already dismissed.

@AbysmalBiscuit
AbysmalBiscuit dismissed github-actions[bot]’s stale review October 16, 2026 17:11

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.

@AbysmalBiscuit
AbysmalBiscuit merged commit 5d9ca92 into main Oct 16, 2026
23 checks passed
@AbysmalBiscuit
AbysmalBiscuit deleted the flaky-windows-tests branch October 16, 2026 17:11
AbysmalBiscuit added a commit that referenced this pull request Oct 16, 2026
🤖 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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant