Skip to content

chore(depot-client): migrate to worker thread#4907

Closed
NathanFlurry wants to merge 1 commit intodriver-test-complaints/use-vitest-matrix-filterfrom
05-03-chore_depot-client_migrate_to_worker_thread
Closed

chore(depot-client): migrate to worker thread#4907
NathanFlurry wants to merge 1 commit intodriver-test-complaints/use-vitest-matrix-filterfrom
05-03-chore_depot-client_migrate_to_worker_thread

Conversation

@NathanFlurry
Copy link
Copy Markdown
Member

Description

Please include a summary of the changes and the related issue. Please also include relevant motivation and context.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Copy link
Copy Markdown
Member Author

NathanFlurry commented May 3, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 3, 2026

PR Review: chore(depot-client): migrate to worker thread

Overview

This PR replaces the async NativeConnectionManager (with a reader/writer connection pool, statement classification, and read routing) with a single dedicated OS thread (SqliteWorkerHandle) per database. SQL commands are dispatched via a bounded crossbeam channel; close requests travel a separate channel so a full SQL queue can't block shutdown. This is a significant architectural simplification that aligns well with SQLite's threading expectations.


What's Good

  • Correct threading model. SQLite is happiest with a single thread owning each connection. Replacing spawn_blocking per query with a persistent worker thread eliminates per-call overhead and makes the lifecycle deterministic.
  • Explicit backpressure. Bounded queue (128) with immediate actor.overloaded error on full is the right pattern per CLAUDE.md — no hidden blocking.
  • Close correctness. Close bypasses the SQL queue via its own bounded(1) channel, so a backlogged queue can't delay shutdown. drop(db) happens before STATE_CLOSED is set, which is the right order.
  • Panic isolation. catch_unwind on the worker thread correctly transitions to STATE_DEAD and notifies all waiters.
  • Async-first initial page fetch. Moving fetch_initial_main_page to an async call before VFS registration eliminates the previous block_on inside a synchronous VFS callback. The block_on_runtime helper in test code is clearly gated behind #[cfg(test)].
  • Ready-signal plumbing. The ready_tx/ready_rx oneshot used to surface connection-open errors to initialize() is clean and avoids silent failures.
  • Test infrastructure. Pause and Panic commands are properly gated behind #[cfg(test)] and give direct control over worker state without adding production overhead.

Issues and Suggestions

1. map_err(anyhow::Error::msg) instead of .context() (minor, style)

open_worker_connection in worker.rs uses map_err(anyhow::Error::msg) twice:

open_connection(...).map_err(anyhow::Error::msg)?;
configure_connection_for_database(...).map_err(anyhow::Error::msg)?;

Per CLAUDE.md: "Prefer .context() over the anyhow! macro." The idiomatic form here is .map_err(anyhow::Error::from) or wrapping with .context("open worker connection"). Using anyhow::Error::msg technically requires AsRef<str> which may not compile if the source error only implements Display (not AsRef<str>). If the error source implements std::error::Error, prefer anyhow::Error::from.

2. Duplicated panic_message function (minor)

panic_message is now defined in both worker.rs and vfs.rs with identical bodies. This could be pulled into a shared pub(crate) function in lib.rs or a small internal utility module.

3. Performance regression: single writer, no readers (acknowledged, but worth flagging)

The previous architecture supported up to 4 concurrent read connections. The new single worker serializes all reads and writes. The todo at .agent/todo/sqlite-parallel-read-workers.md tracks this, but for read-heavy actors this is a step backward. The CLAUDE.md constraint says "Native SQLite read mode may hold multiple read-only connections" — this PR temporarily violates that invariant. The spec acknowledges v1 is intentionally single-threaded, but it's worth calling out explicitly in the PR description.

4. execute_write removed without a clear deprecation path

NativeDatabaseHandle::execute_write is gone. If any NAPI callers relied on forcing write-route classification, they now silently use execute() through the single worker. This is functionally fine since the worker is the only writer, but the silent removal of an explicit write-routing guarantee could confuse future callers.

5. Statement classification tests deleted

tests/statement_classification.rs (198 lines) is fully removed. These tests covered classify_statement, StatementAuthorizerSummary, and reader-eligibility logic. Even though that code is gone now, if read workers are reintroduced later, those tests will need to be recreated from scratch. Noting the deletion in the todo file would reduce future friction.

6. wait_ready() take-once semantics are drop-unsafe on cancellation

pub async fn wait_ready(&self) -> Result<()> {
    let ready = self.inner.ready.lock().take();
    let Some(ready) = ready else { return Ok(()); };
    // ...
}

This is correct for the single-caller case. However, if this future is dropped after the lock take but before .await is polled, the receiver is dropped silently and all future calls return Ok(()) — masking a connection failure. In practice initialize() calls this once, so the risk is low, but a comment explaining the once-semantics would prevent future surprises.

7. Close timeout leaves worker thread alive (acknowledged in code)

The comment in close() correctly notes that the VFS cannot be unregistered while SQLite may still be inside a callback. The background join via join_worker_in_background is the right approach. No action needed, just confirming this is intentional and well-documented.


Minor Style Notes

  • Most of the diff in test files is purely formatting churn (line wrapping, argument alignment). Fine but adds noise to the review.
  • New metrics in SqliteVfsMetricsSnapshot (page_cache_entries, write_buffer_dirty_pages, db_size_pages) are a nice addition for observability.
  • The PR description template is unfilled — adding a short summary of the motivation (avoid spawn_blocking per query, align with SQLite threading model) and test coverage would help future reviewers.

Summary

The core design is sound and a clear improvement over the async connection manager for SQLite's threading model. Two items worth addressing before merge:

  1. Verify map_err(anyhow::Error::msg) compiles correctly for all error source types, or switch to .context() / anyhow::Error::from.
  2. Deduplicate panic_message across worker.rs and vfs.rs.

The acknowledged throughput regression (single writer) is acceptable for v1 given the todo tracking parallel read workers.

🤖 Generated with Claude Code

@NathanFlurry NathanFlurry force-pushed the driver-test-complaints/use-vitest-matrix-filter branch from bf25094 to 123cbb9 Compare May 3, 2026 23:40
@NathanFlurry NathanFlurry force-pushed the 05-03-chore_depot-client_migrate_to_worker_thread branch from 5aa51ce to 8f30b24 Compare May 3, 2026 23:40
@NathanFlurry NathanFlurry marked this pull request as ready for review May 4, 2026 00:20
@NathanFlurry NathanFlurry force-pushed the driver-test-complaints/use-vitest-matrix-filter branch from 123cbb9 to bf25094 Compare May 4, 2026 00:32
@NathanFlurry NathanFlurry force-pushed the 05-03-chore_depot-client_migrate_to_worker_thread branch from 8f30b24 to b5f770f Compare May 4, 2026 00:32
@NathanFlurry NathanFlurry force-pushed the 05-03-chore_depot-client_migrate_to_worker_thread branch from b5f770f to 8ffd1e8 Compare May 4, 2026 04:13
@NathanFlurry NathanFlurry force-pushed the driver-test-complaints/use-vitest-matrix-filter branch from bf25094 to 6377945 Compare May 4, 2026 04:13
@NathanFlurry
Copy link
Copy Markdown
Member Author

Landed in main via stack-merge fast-forward push. Commits are in main; closing to match.

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