microsandbox_db/pool.rs
1//! Canonical SQLite connection builder shared by every microsandbox process.
2//!
3//! Both the host CLI and the in-VM runtime open the same SQLite file and
4//! must apply identical PRAGMAs (WAL, busy timeout, foreign keys,
5//! synchronous=NORMAL). Centralising the builder keeps that contract in
6//! one place — when a new PRAGMA is needed, this is the only file to edit.
7
8use std::{path::Path, time::Duration};
9
10use sea_orm::{DatabaseConnection, SqlxSqliteConnector};
11use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous};
12
13use crate::connection::{DbReadConnection, DbWriteConnection};
14
15/// Default `busy_timeout` PRAGMA value used when a caller has no
16/// user-facing knob to plumb (e.g. the in-VM runtime, where the host
17/// owns DB-tuning policy and the runtime is not user-configurable).
18pub const DEFAULT_BUSY_TIMEOUT_SECS: u64 = 5;
19
20/// Read pool + dedicated single-connection write pool for the same SQLite
21/// file. SQLite is single-writer system-wide, so a multi-connection pool
22/// fighting for the writer lock just generates `SQLITE_BUSY` and (under
23/// deferred transactions) `SQLITE_BUSY_SNAPSHOT`. Funnelling all writes
24/// from one process through a single connection turns within-process
25/// contention into an in-process queue (deterministic) instead of
26/// SQLite-level lock contention (probabilistic, retry-required). Reads
27/// keep the wider pool — WAL mode lets readers run concurrently.
28#[derive(Debug)]
29pub struct DbPools {
30 read: DbReadConnection,
31 write: DbWriteConnection,
32}
33
34impl DbPools {
35 /// Open both pools for the SQLite file at `db_path` with shared PRAGMAs.
36 ///
37 /// The write pool connects first so WAL mode (persisted in the database
38 /// header) is set before the read pool opens. `max_read_connections`
39 /// sizes only the read pool; the write pool is always single-connection
40 /// by design.
41 pub async fn open(
42 db_path: &Path,
43 max_read_connections: u32,
44 connect_timeout: Duration,
45 busy_timeout: Duration,
46 ) -> Result<Self, sqlx::Error> {
47 let write = DbWriteConnection::open(db_path, connect_timeout, busy_timeout).await?;
48 let read =
49 DbReadConnection::open(db_path, max_read_connections, connect_timeout, busy_timeout)
50 .await?;
51 Ok(Self { read, write })
52 }
53
54 /// Borrow the read pool (multi-connection).
55 pub fn read(&self) -> &DbReadConnection {
56 &self.read
57 }
58
59 /// Borrow the write pool (single-connection, retries handled inside).
60 pub fn write(&self) -> &DbWriteConnection {
61 &self.write
62 }
63}
64
65/// Open a sqlx-backed SQLite pool wrapped as a sea-orm `DatabaseConnection`.
66///
67/// PRAGMAs are applied to every connection in the pool via
68/// `SqliteConnectOptions`, so callers don't need to issue any setup SQL.
69///
70/// `busy_timeout` is how long SQLite will spin internally on a contended
71/// lock before returning `SQLITE_BUSY`. It interacts with the
72/// application-level retry policy: a longer busy timeout reduces retry
73/// volume at the cost of higher tail latency on contention.
74pub(crate) async fn build_pool(
75 db_path: &Path,
76 max_connections: u32,
77 connect_timeout: Duration,
78 busy_timeout: Duration,
79) -> Result<DatabaseConnection, sqlx::Error> {
80 let connect_options = SqliteConnectOptions::new()
81 .filename(db_path)
82 .create_if_missing(true)
83 .journal_mode(SqliteJournalMode::Wal)
84 .busy_timeout(busy_timeout)
85 .foreign_keys(true)
86 .synchronous(SqliteSynchronous::Normal);
87
88 let pool = SqlitePoolOptions::new()
89 .max_connections(max_connections)
90 .acquire_timeout(connect_timeout)
91 .connect_with(connect_options)
92 .await?;
93
94 Ok(SqlxSqliteConnector::from_sqlx_sqlite_pool(pool))
95}