Repository navigation
Conversation
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR adds a substantial worker-thread SQLite execution path and enables it by default for the production server, affecting transaction routing, concurrency, resource usage, and snapshot behavior. It also adds a line-level lint suppression, so the change requires human review. No code changes detected at You can add or adjust custom eligibility rules. Learn more. |
8330f48 to
f24db69
Compare
|
Warning Review limit reachedOnly developers with an assigned seat can use this organization's usage-based review budget, and seats here are assigned manually. Ask an admin to assign a seat, or change the review continuation mode in Billing. Next included review available in 21 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (6)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review. 📝 WalkthroughWalkthroughThe SQLite client can route effects marked read-only to a reader worker for eligible file-backed databases. Server shell snapshot transactions now use this routing, and the tests cover worker startup, transaction behavior, and fallback to the writer. ChangesSQLite read-only snapshots
Priority: ⬆️ High Estimated code review effort: 4 (Complex) | ~45 minutes Change: Bug fix · Severity of issue fixed: High Sequence Diagram(s)sequenceDiagram
participant getShellSnapshot
participant NodeSqliteClient
participant ReaderWorker
getShellSnapshot->>NodeSqliteClient: mark snapshot transaction read-only
NodeSqliteClient->>ReaderWorker: execute read request
ReaderWorker-->>NodeSqliteClient: return query result
NodeSqliteClient-->>getShellSnapshot: return snapshot data
Suggested reviewers: Merge Risk: ⚪ Minimal · up to Eligible snapshots can use the reader worker, while unavailable-reader cases retain the existing writer path. No actionable merge-blocking regression is established. Architecture SummaryArchitecture risk: 🔵 Low · up to The change affects 2 systems. Changed systems: Architecture concerns Review detailsSystems and components
Before / after behavior
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Comment |
f24db69 to
7a3312d
Compare
|
Review requested
Logged so this PR shows when a maintainer was asked to review it. |
e7aa57e to
6a3edae
Compare
…atabases getShellSnapshot held the only SQLite connection on the main thread for the whole read, blocking keepalives, requests and writes for seconds on large databases. nodeSqliteClient gets a readerWorker option and a readOnly marker: a transaction wrapped in readOnly runs on a second connection in a worker thread. getShellSnapshot and the three routes that wrap it in a transaction are marked. The reader connection is opened read-only, because PRAGMA query_only can be turned off by a statement on the same connection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cannot start The reader worker opened the database lazily, so a read-only open failure broke every thread list read for good, and a database outside WAL mode let a reader's shared lock stall the writer. The worker now opens the file read-only at startup, checks for WAL, and reports ready before any read is routed to it. If it cannot, does not report within readerStartupTimeout (10 s), or a request is cancelled while it starts, reads use the main connection as before; the reader turns off once, with one warning. A late "ready" from a worker another read already retired is not handed out. getShellSnapshot is no longer marked inside the store. Callers opt in where a snapshot that keeps moving is fine: the three snapshot routes and the once-a-minute pull request discovery pass, which guards its writes with the snapshot's sequence. Restore-safety and project-deletion checks read on the main connection, as on main. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
6a3edae to
3f31329
Compare
|
Review requested
Logged so this PR shows when a maintainer was asked to review it. |
Problem
On a large database, reading the thread list freezes the whole server for seconds.
getShellSnapshotruns on every client connect and once a minute from the pull request discovery pass. It holds the only SQLite connection, on the main thread, for the whole read, so websocket keepalives, other requests, and writes all wait. On a clone of a 33 GB database (about 3,400 threads), each snapshot blocked the event loop for 1.4 to 2.2 s. Clients miss keepalives and reconnect, and each reconnect reads the snapshot again. Details in #14701.Change
nodeSqliteClientgets areaderWorkeroption and areadOnlymarker. A transaction wrapped inreadOnlyruns on a second, read-only connection in a worker thread, so it blocks neither the event loop nor the writer. Everything unmarked keeps using the existing connection.ws.ts) and the HTTP snapshot (http.ts). Each still reads projects, threads and the application sequence in one transaction.ThreadPullRequestService). It passes the snapshot's own sequence to its existing stale-write guard.getShellSnapshotitself is not marked. Checks that act on the result, such as checkpoint restore safety and project deletion, read on the writer exactly as on main.Sqlite.ts).How the reader behaves:
PRAGMA query_only) and checks the database is in WAL mode, then reports ready.readerStartupTimeout(10 s by default);readOnlycode inside an open write transaction also stays on the writer.BEGIN: a write on the reader fails with "attempt to write a readonly database". Main now opens writer transactions withBEGIN IMMEDIATE; the reader rewrites anyBEGINto a plain deferredBEGIN, so marked reads never take the write lock.SqlErrorinstead of continuing on a new worker.COMMITandROLLBACKthen succeed as no-ops, which is safe because the reader never writes. The next read starts a new worker.Scope and approval
Fixes #14701, triaged as a bug by @juliusmarminge on 2026-10-02 (triage comment). Of the three fixes suggested there, this is "don't hold the only connection for the whole read", and it takes the discovery pass off the main thread. The other two (store the turn-item counts on the thread row; give the discovery pass a narrower query) would make the read itself cheaper. They are separate changes and are not attempted here. The change is limited to the SQLite adapter, the four marked callers, and the server's layer config.
Verification
A/B on a clone of a real 33 GB
statev2.sqlite(APFS clone of a consistent backup; 3,347 threads; realgetShellSnapshotthroughProjectionStore.layerandmakeSqlitePersistenceLive). Base and candidate ran in alternating rounds, 12 snapshots each. The main-thread gap was recorded with a 2 mssetInterval. In parallel, a probe ranSELECT 1on the writer every 25 ms.The A/B above ran before the reader switched from
PRAGMA query_onlyto a read-only open. One rerun of the candidate after that switch, with the same setup, gave a longest gap of 12 to 21 ms over 6 snapshots.monitorEventLoopDelayunder-reported the base stall in this setup (max about 30 ms). The timer gap and the probe log agree with each other and with the 1.4 to 2.6 s stalls seen in live server traces.Stress test on GCP. A synthetic 36 GB
statev2.sqliteshaped like ours: 3,441 threads, 253k turn items, and a heavy tail of 8 threads with over 2,000 items. Each server ran on Linux with a fake Codex provider, and a load driver supplied websocket clients, writers, and reconnect storms. Base and candidate alternated on the same VM, 3 repetitions each. Values are medians [min, max].PRAGMA quick_checkreturned ok.The A/B and stress numbers were measured at
8330f48362a, wheregetShellSnapshotwas marked inside the store. Those runs exercised the snapshot routes, which are marked the same way now. The discovery pass was not measured on the reader.Focused tests on the current head
vp test run packages/shared/src/nodeSqliteClient.test.ts: 21 passed, 13 of them added by this PR.readerWorker: false, three of the core tests fail: a writer commit during an open read deadlocks, a write on the reader is accepted, and a crashed reader reports no error.vp test runonThreadPullRequestService.test.ts,Sqlite.test.ts,ProjectionStore.test.ts,PullRequestSyncReactor.test.ts,SessionStore.test.ts,ProjectSettingsUpgrade.integration.test.ts,OrchestratorReplayRecovery.integration.test.tsandOrchestratorReplayRestartBackgroundNote.integration.test.ts: 81 passed. Several use a real database file, so they go through the reader.tsc --noEmitforpackages/sharedandapps/server: no errors.vp lint,vp fmt --checkandknip --exportsfor the touched workspaces: clean.Runtimes: an inline-source worker that opens
node:sqliteand reads rows works under the desktop app's Electron 44.4.2 (ELECTRON_RUN_AS_NODE) and in a Node 26.7 single-executable build.What this can make worse
These are known costs or exposures, not defects found in testing:
journal_size_limit(32 MB) trims the file only after a reset, so it is not a cap. The stress runs peaked at 49 MB during reconnect storms, against 4 MB on base.getSettlementCandidates) and the pull request sync scan (getThreadsWithPullRequests). Each can be marked separately once measured.Not checked:
Reviewed by GPT-6 Astra (cross-vendor): approved the original change after two rounds; a risk review of the merged branch led to the startup check and the per-caller marking; re-reviews of those led to the startup deadline, the interruptible wait and the single turn-off, and then to the late-ready recheck. The recheck has a test that fails without it but was not reviewed again. Macroscope found that SQL could turn
query_onlyback off; the reader opens read-only. The 2026-10-05 rebase was independently reviewed by GPT-6.1 Sol with no actionable findings.Claude Opus 5.5 in T3 Code (Claude Code harness).
🤖 Generated with Claude Code