Repository navigation
fix(shared): let SQLite reads proceed beside slow reads - #24762
juliusmarminge wants to merge 3 commits into
Conversation
A read-only query waits for an unrelated open read-only transaction because every reader query shares one reader permit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All reader work shared one worker behind one permit, so a read-only transaction held every unrelated read behind it. Reads now lease one of up to four reader workers, started lowest slot first and only under contention. A transaction keeps its leased worker until its scope closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApprovabilityVerdict: Needs Human Approval Macroscope's review found this PR requires human approval — This production change replaces the single SQLite reader with a lazily initialized four-worker pool and adds interruption-safe leasing across standalone reads and transactions. Although the regression test is focused and the intent is clear, the concurrency and resource-lifecycle changes are substantial enough to require human review. You can add or adjust custom eligibility rules. Learn more. |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Reader pool and lease lifecycle packages/shared/src/nodeSqliteClient.ts |
The client uses four reader-worker slots with a semaphore to manage leases. It starts or reuses workers by slot, clears stopped workers, terminates workers at shutdown, and releases the slot and permit after use. |
Read routing and transaction acquisition packages/shared/src/nodeSqliteClient.ts, packages/shared/src/nodeSqliteClient.test.ts |
Read-only operations lease a reader when available and otherwise use the writer. Writer borrowing retains checkpoint-worker batching and yield behavior. Transaction scopes register lease or writer-lock cleanup. The regression test checks that a second read-only query completes while a read-only transaction remains open. |
Priority:
Estimated code review effort: 4 (Complex) | ~45 minutes
Change: Bug fix · Severity of issue fixed: Medium
Sequence Diagram(s)
sequenceDiagram
participant Caller
participant borrower
participant makeReader
participant SQLiteReaderWorker
Caller->>borrower: Run read-only operation
borrower->>makeReader: Acquire reader lease
makeReader->>SQLiteReaderWorker: Start or reuse worker for slot
SQLiteReaderWorker-->>borrower: Provide reader connection
borrower-->>Caller: Return operation result and release lease
Possibly related issues
- [Bug]: Thread snapshot HTTP handlers have no server-side timeout; cold-open herd times out at 6s and floods relay logs with context-canceled warnings #17765 — This change adds multiple reader workers, which addresses reads queuing behind one slow reader. It does not add the server timeout or client concurrency limit described in the issue.
Possibly related PRs
- pingdotgg/t3code#24586 — Both changes cover SQLite reader concurrency and checkpoint behavior while a read snapshot remains open.
- pingdotgg/t3code#20877 — Both changes use reader workers for SQLite operations; this PR changes the worker design to a bounded pool and updates transaction acquisition.
Suggested labels: size:M
Suggested reviewers: yash-singh1, t3dotgg
Merge Risk: 
Minimal · up to 37df3
-
This change lets independent SQLite reads run beside slow reads by using up to four reader workers, and it keeps the writer fallback. No concrete merge-blocking defect was identified. The reported tests and typecheck passed.
Pre-merge checks: 
3 · 
1
-
Failed checks: 1 warning Passed checks: 3 passed-
Full details: Out of Scope Changes check
-
Explanation
The changes are in packages/shared/src/nodeSqliteClient.ts and its test file, per the summary and the diff. The reader pool (slots, permit semaphore, lease release, PreventSchedulerYield around the permit take) implements
#24660Request 2. The writer-path refactor (borrowWriter, transactionAcquirer, annotation helpers) supports this change. It lets reads and transactions choose reader or writer through one path and keeps writer fallback. The regression test for head-of-line blocking supports the reader-serialization objective. The diff output was truncated at 16000 bytes, so the transactionAcquirer hunk was read only from the summary. The summary describes it as reader leasing with writer fallback, which matches the pool design. No change outside the reader and writer path was found.
-
Full details: Linked Issues check
-
Explanation
Issue
#24660has two coding requests. Request 2 (more reader concurrency) is implemented. In packages/shared/src/nodeSqliteClient.ts, the single reader and Semaphore.make(1) are replaced by READER_WORKERS = 4 slots and a permit semaphore. Read-only effects lease a reader, and transactions keep one until their scope closes. Request 1 is not implemented. The issue asks to cache the negative result in ensureTranscript when no legacy-import row exists. The base-to-head diff for apps/server/src/orchestration-v2/legacy/LegacyV1ThreadImporter.ts is empty, so no negative cache was added. The PR objectives and description state that this fix belongs to#24707. A reference to#24707does not show its code is in this head. The PR also says 'Fixes#24660', which would close the issue while Request 1 is still unmet. Four readers also still queue extra work, which the PR states; this is accepted under Request 2's 'small pool' option.Resolution
Either (a) implement Request 1 in this PR: in LegacyV1ThreadImporter.ts ensureTranscript, when no legacy-import row exists, add the thread id to confirmedTranscriptThreadIds before returning, and add a regression test that a second ensureTranscript call for a V2-created thread does not re-run the legacy query; or (b) remove 'Fixes
#24660' from the PR description, reference#24660only as partial progress, and keep the issue open until the negative-cache change is merged.
-
Finishing Touches
-
📝 Generate docstrings
-
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
-
- Commit to this branch
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
|
Production-size measurement from a fork, since the description says no production-sized claim is made. Setup: a personal fork of nightly Reader spans from the server's trace files,
Before the change, lock wait was nearly the whole span duration, so the latency was queueing rather than query cost. A bench on a snapshot copy of the same database, with a 100 ms session-row probe running while thread-history scans loop, adds two points the description touches on but does not quantify:
After the pool, individual reader spans still reach p90 424 ms, so the scans themselves remain expensive. #26240 looks like the complementary half. Landing note: against the current nightly's Effect language-service rules this head trips three diagnostics, Nothing in the comment is derived from the reporter's database. Measurement script and bench are reproducible from the trace files alone. |
|
Note 🤖 Claude Opus 5.5 responding on behalf of Theo Closing: this is covered by #27097, which landed. Reopen if a needed part is still missing. |
Problem
On a large server, a slow shell read holds the only SQLite reader permit and delays unrelated thread reads. On current main with an isolated 4,122-thread fixture, a native-thread legacy lookup spent 181–220 ms acquiring the reader while a shell transaction held it for 203–241 ms.
Change
Use a bounded pool of four reader workers, started lazily as concurrent reads need them. Each transaction retains one worker until its scope closes; standalone reads return their lease on completion or interruption. Protect the permit handoff against scheduler yields so cancellation cannot lose a lease. Existing writer routing and fallback remain available.
Scope and approval
Fixes #24660
GitCafe: https://git.cafe/pingdotgg/t3code/issues/24660
Triaged bug via the automated issue flow. This PR addresses reader serialization. The separate native-V2 negative transcript cache fix remains in #24707.
Verification
vp test run packages/shared/src/nodeSqliteClient.test.ts apps/server/src/persistence/Sqlite.test.ts apps/server/src/persistence/DatabaseMaintenance.test.ts: 22 tests passed across three files.Runtime report · Sanitized seed/reproduction drivers, focused spans, and transcripts
Runtime verification used the real dev server and HTTP/WebSocket APIs on Linux with isolated synthetic state (about 270 MB, one million completed items), rather than the reporter's 16–25 GB database. Four occupied readers can still queue additional work. The missing legacy lookup still repeats; shell query cost is unchanged. The runtime workload does not force the narrow scheduler race, and its regression coverage is an external review probe rather than a committed test. No production-sized performance, universal latency, or permanent-hang claim is made. Other host operating systems were not run locally.
Model: GPT-6.1-Sol and Claude Opus 5.5. Harness: Codex and Claude Code through T3 Code.