Skip to content

refactor(db): typed read and write connection types#668

Merged
toksdotdev merged 3 commits intodb/split-pools-retryfrom
db/typed-connections
May 4, 2026
Merged

refactor(db): typed read and write connection types#668
toksdotdev merged 3 commits intodb/split-pools-retryfrom
db/typed-connections

Conversation

@toksdotdev
Copy link
Copy Markdown
Member

@toksdotdev toksdotdev commented May 4, 2026

stacks on #666.

summary

we already split each database into a read pool and a write pool, but both were just sea_orm::DatabaseConnection. nothing in the type system stopped a write from sneaking onto the read pool, and the retry-on-busy logic had to look up the right pool dynamically. this pr makes the read/write split a property of the type.

DbReadConnection and DbWriteConnection are newtypes over the underlying connection. both implement ConnectionTrait so existing query builders work unchanged, but write transactions are only available on DbWriteConnection. every host call site now declares its intent in its signature: read helpers take &DbReadConnection, write helpers take &DbWriteConnection, and the few that do both take &DbPools.

with the typed handles in place, the dynamic write-pool lookup and the per-call max-connections override are gone, and two writes that were managing their own transactions pick up retry-on-busy for free.

a 200-concurrent-boot benchmark shows a ~18% throughput improvement over the previous design (21.3 sandboxes/sec vs 18.2), with median wall time falling from ~6992 ms to ~5950 ms. zero SQLITE_BUSY errors observed in either run.

test plan

  • cargo test -p microsandbox --lib
  • cargo check --workspace --all-targets

toksdotdev added 3 commits May 4, 2026 16:45
split sea_orm::DatabaseConnection into two newtypes in microsandbox-db:
DbReadConnection (multi-conn read pool) and DbWriteConnection
(single-conn write pool, retries built in). both implement
ConnectionTrait so existing query builders work transparently; the
type system enforces which pool a given operation hits.

add DbPools::open as the canonical constructor for paired pools, plus
DbReadConnection::open and DbWriteConnection::open for stand-alone
single-pool callers (e.g. the in-VM runtime).

DbWriteConnection::transaction wraps begin/commit/rollback with
SQLITE_BUSY retry. generic over the closure error type via a new
IsSqliteBusy trait so transaction bodies can return app-level errors
(MicrosandboxError, RuntimeError) directly. retry_on_busy in
microsandbox-db is now generic over the same trait.

migrate microsandbox-runtime to DbWriteConnection end-to-end:
connect_db returns the typed handle, insert_run / mark_run_failed /
run_metrics_sampler / persist_sample take it.

implement IsSqliteBusy for MicrosandboxError and RuntimeError.

the host microsandbox crate is intentionally left on the old
&DatabaseConnection API in this commit; the call-site sweep follows.
migrate every microsandbox call site from sea_orm::DatabaseConnection
to the typed DbReadConnection / DbWriteConnection wrappers introduced
in the previous commit. function signatures now declare intent
(read vs write) and writes go through DbWriteConnection::transaction
so retry-on-busy is consistent.

db::init_global / db::init_project now return &'static DbPools and
read all tuning (max_connections, connect_timeout_secs,
busy_timeout_secs) from the global config rather than taking a per-call
override. drops the unused max_connections parameter that every
caller was setting to the same config value.

internal helpers split by responsibility:
- read-only (load_active_run, load_active_pids, load_sandbox_record,
  build_handle, latest_metric, metrics_for_sandbox) take
  &DbReadConnection
- write-only (update_sandbox_status, mark_sandbox_runtime_stale,
  mark_sandbox_stopped_for_replacement, insert_sandbox_record,
  persist_oci_manifest_pin) take &DbWriteConnection
- mixed read+write (reconcile_sandbox_runtime_state,
  load_sandbox_record_reconciled, prepare_create_target,
  stop_sandbox_for_replacement) take &DbPools

removes the local with_retry_transaction shim and the
write_pool_for pointer-equality lookup; both are now obviated by the
typed handles. mark_sandbox_runtime_stale and
mark_sandbox_stopped_for_replacement gain SQLITE_BUSY retry by virtue
of moving onto DbWriteConnection::transaction.

build_pool in microsandbox-db is tightened back to pub(crate) since
constructors (DbPools::open, DbWriteConnection::open,
DbReadConnection::open) are now the public surface.

test boilerplate consolidated behind a small open_test_pools helper.
init_project, project(), and the PROJECT_POOL OnceCell had no callers
in the workspace. the path was carved out for a future
<project>/.microsandbox/db/msb.db use case but never wired up; the
host always reads/writes the global DB at ~/.microsandbox/db/msb.db.

remove the dead code along with the now-unused PathBuf import. easy
to add back if/when project-local databases land.
@toksdotdev toksdotdev changed the title refactor(db): typed DbReadConnection / DbWriteConnection refactor(db): typed read and write connection types May 4, 2026
@toksdotdev toksdotdev marked this pull request as ready for review May 4, 2026 22:19
@toksdotdev toksdotdev requested a review from appcypher as a code owner May 4, 2026 22:19
@toksdotdev toksdotdev merged commit 2102c28 into db/split-pools-retry May 4, 2026
1 check passed
@toksdotdev toksdotdev deleted the db/typed-connections branch May 4, 2026 22:22
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