Skip to main content

runlight_sqlx/
sqlite.rs

1//! SQLite's one connection, held for the store's statements in turn.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use runlight::BoxFuture;
7use runlight::store::{Db, DbError, Dialect, Hold, Param, Row};
8use sqlx::pool::PoolConnection;
9use sqlx::sqlite::{Sqlite, SqliteArguments};
10use sqlx::{AssertSqlSafe, SqlitePool};
11use tokio::sync::{Mutex, OwnedMutexGuard};
12
13use crate::rows::{db_error, sqlite_row};
14
15/// How long opening SQLite keeps retrying a busy database before it gives up.
16const BUSY_RETRY: Duration = Duration::from_secs(2);
17
18/// The store's one connection, opened on first use.
19#[derive(Default)]
20struct Slot {
21    conn: Option<PoolConnection<Sqlite>>,
22    /// A transaction was let go of before it ended (a request cancelled before `finish`), so it is
23    /// rolled back before the connection is used again. Closing the connection instead would lose an
24    /// in-memory database.
25    abandoned: bool,
26}
27
28/// The pool, and the one connection of it the store holds. Statements and transactions take turns, so
29/// a request's insert can never land inside an import's open transaction, and a rollback can only undo
30/// the transaction's own writes.
31#[derive(Clone)]
32pub struct SqliteDb {
33    pool: SqlitePool,
34    conn: Arc<Mutex<Slot>>,
35}
36
37impl SqliteDb {
38    /// The store's driver over a pool.
39    pub fn new(pool: SqlitePool) -> SqliteDb {
40        SqliteDb { pool, conn: Arc::new(Mutex::new(Slot::default())) }
41    }
42}
43
44/// The connection, opened (and its pragmas set) on first use and held for the caller.
45async fn open(pool: &SqlitePool, guard: &mut Slot) -> Result<(), DbError> {
46    if guard.abandoned {
47        if let Some(conn) = guard.conn.as_mut()
48            && let Err(e) = exec(conn, "ROLLBACK").await
49            && !e.to_string().contains("no transaction is active")
50        {
51            // Still in the transaction, perhaps: never used again.
52            if let Some(conn) = guard.conn.take() {
53                drop(conn.detach());
54            }
55        }
56        // Only once the rollback is done, so one cancelled in turn is tried again.
57        guard.abandoned = false;
58    }
59    if guard.conn.is_none() {
60        let mut conn = pool.acquire().await.map_err(db_error)?;
61        // No busy handler until WAL is on: switching journal mode can answer busy at once while
62        // another process is doing the same on a new file, so that is retried.
63        wal(&mut conn).await.map_err(db_error)?;
64        exec(&mut conn, "PRAGMA synchronous = NORMAL").await.map_err(db_error)?;
65        // Deleted rows, such as a day's salt once it is gone, are overwritten where that costs no extra writes,
66        // so they do not linger in the file's free space.
67        exec(&mut conn, "PRAGMA secure_delete = FAST").await.map_err(db_error)?;
68        exec(&mut conn, "PRAGMA busy_timeout = 5000").await.map_err(db_error)?;
69        guard.conn = Some(conn);
70    }
71    Ok(())
72}
73
74fn arguments(params: Vec<Param>) -> Result<SqliteArguments, DbError> {
75    use sqlx::Arguments;
76    let mut args = SqliteArguments::default();
77    for p in params {
78        match p {
79            Param::Null => args.add(Option::<i64>::None),
80            Param::Int(n) => args.add(n),
81            Param::Float(f) => args.add(f),
82            Param::Text(s) => args.add(s),
83        }
84        .map_err(db_error)?;
85    }
86    Ok(args)
87}
88
89fn broken(err: &sqlx::Error) -> bool {
90    matches!(err, sqlx::Error::Io(_) | sqlx::Error::PoolClosed | sqlx::Error::WorkerCrashed)
91}
92
93async fn all_on(pool: &SqlitePool, slot: &mut Slot, sql: &str, params: Vec<Param>) -> Result<Vec<Row>, DbError> {
94    open(pool, slot).await?;
95    let conn = slot.conn.as_mut().expect("an open connection");
96    let result = sqlx::query_with(AssertSqlSafe(sql.to_string()), arguments(params)?).fetch_all(&mut **conn).await;
97    if let Err(e) = &result
98        && broken(e)
99    {
100        slot.conn = None;
101    }
102    result.map_err(db_error)?.iter().map(sqlite_row).collect()
103}
104
105async fn run_on(pool: &SqlitePool, slot: &mut Slot, sql: &str, params: Vec<Param>) -> Result<u64, DbError> {
106    open(pool, slot).await?;
107    let conn = slot.conn.as_mut().expect("an open connection");
108    let result = sqlx::query_with(AssertSqlSafe(sql.to_string()), arguments(params)?).execute(&mut **conn).await;
109    if let Err(e) = &result
110        && broken(e)
111    {
112        slot.conn = None;
113    }
114    Ok(result.map_err(db_error)?.rows_affected())
115}
116
117impl Db for SqliteDb {
118    fn dialect(&self) -> Dialect {
119        Dialect::Sqlite
120    }
121
122    fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
123        Box::pin(async move {
124            let mut slot = self.conn.lock().await;
125            all_on(&self.pool, &mut slot, sql, params).await
126        })
127    }
128
129    fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
130        Box::pin(async move {
131            let mut slot = self.conn.lock().await;
132            run_on(&self.pool, &mut slot, sql, params).await
133        })
134    }
135
136    fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
137        Box::pin(async move {
138            // SQLite's file lock already serialises its writers, so a lock to create tables needs nothing.
139            if kind == Hold::Exclusive {
140                return Ok(Arc::new(self.clone()) as Arc<dyn Db>);
141            }
142            // Open from before BEGIN is sent, since a BEGIN cancelled part way may still have run.
143            let mut tx = Tx { slot: self.conn.clone().lock_owned().await, open: true };
144            run_on(&self.pool, &mut tx.slot, "BEGIN", vec![]).await?;
145            Ok(Arc::new(Held { pool: self.pool.clone(), guard: Arc::new(Mutex::new(Some(tx))), owner: true })
146                as Arc<dyn Db>)
147        })
148    }
149}
150
151/// The connection, held for one transaction until it ends. Let go of while still open, it has the
152/// transaction rolled back before anything else uses the connection.
153struct Tx {
154    slot: OwnedMutexGuard<Slot>,
155    open: bool,
156}
157
158impl Drop for Tx {
159    fn drop(&mut self) {
160        if self.open {
161            self.slot.abandoned = true;
162        }
163    }
164}
165
166/// The connection held for one transaction. A transaction asked for inside it joins it: the same
167/// connection, with nothing to finish.
168struct Held {
169    pool: SqlitePool,
170    guard: Arc<Mutex<Option<Tx>>>,
171    owner: bool,
172}
173
174impl Drop for Held {
175    fn drop(&mut self) {
176        // Its owner gone without `finish`: the connection is let go now rather than when the last joined
177        // handle goes, so other statements are not held up behind it.
178        if self.owner
179            && let Ok(mut held) = self.guard.try_lock()
180        {
181            held.take();
182        }
183    }
184}
185
186fn ended() -> DbError {
187    DbError("Runlight: the transaction has ended".into())
188}
189
190impl Db for Held {
191    fn dialect(&self) -> Dialect {
192        Dialect::Sqlite
193    }
194
195    fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>> {
196        Box::pin(async move {
197            let mut held = self.guard.lock().await;
198            let slot = &mut held.as_mut().ok_or_else(ended)?.slot;
199            all_on(&self.pool, slot, sql, params).await
200        })
201    }
202
203    fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>> {
204        Box::pin(async move {
205            let mut held = self.guard.lock().await;
206            let slot = &mut held.as_mut().ok_or_else(ended)?.slot;
207            run_on(&self.pool, slot, sql, params).await
208        })
209    }
210
211    fn hold(&self, _kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>> {
212        // A transaction already open is joined.
213        Box::pin(async move {
214            Ok(Arc::new(Held { pool: self.pool.clone(), guard: self.guard.clone(), owner: false }) as Arc<dyn Db>)
215        })
216    }
217
218    fn finish(&self, ok: bool) -> BoxFuture<'_, Result<(), DbError>> {
219        Box::pin(async move {
220            if !self.owner {
221                return Ok(());
222            }
223            let mut held = self.guard.lock().await;
224            let Some(mut tx) = held.take() else { return Ok(()) };
225            let result = run_on(&self.pool, &mut tx.slot, if ok { "COMMIT" } else { "ROLLBACK" }, vec![]).await;
226            // One that failed may still be in its transaction, which is rolled back before the connection
227            // is used again.
228            tx.open = result.is_err();
229            result.map(|_| ())
230        })
231    }
232}
233
234/// Runs a statement whose rows, if any, are not wanted (`PRAGMA journal_mode` answers one).
235async fn exec(conn: &mut PoolConnection<Sqlite>, text: &'static str) -> Result<(), sqlx::Error> {
236    sqlx::query(text).fetch_all(&mut **conn).await.map(|_| ())
237}
238
239fn is_busy(err: &sqlx::Error) -> bool {
240    let text = err.to_string();
241    let code = match err {
242        sqlx::Error::Database(db) => db.code().map(|c| c.to_string()).unwrap_or_default(),
243        _ => String::new(),
244    };
245    // SQLITE_BUSY is 5 and SQLITE_LOCKED 6, and their extended codes are those plus a multiple of 256.
246    let busy_code = code.parse::<i64>().is_ok_and(|c| c & 0xff == 5 || c & 0xff == 6);
247    busy_code || text.contains("SQLITE_BUSY") || text.contains("database is locked")
248}
249
250/// Makes the database file, when there is none yet, for its owner and group alone (0660), whatever the
251/// umask: the visits in it are nobody else's to read, and a scheduled job running as another user of the
252/// same group can still write to it. It is made before SQLite opens it and turns on WAL, so the -wal and
253/// -shm files SQLite makes beside it take the same mode. Systems without such modes make it as it is.
254pub(crate) fn own_file(path: &std::path::Path) {
255    if path.exists() {
256        return;
257    }
258    // A file another process made a moment ago is left as it is.
259    let Ok(file) = std::fs::OpenOptions::new().write(true).create_new(true).open(path) else { return };
260    #[cfg(unix)]
261    {
262        use std::os::unix::fs::PermissionsExt;
263        // Not a file this process may change, or a file system with no such modes: it stays as made.
264        let _ = file.set_permissions(std::fs::Permissions::from_mode(0o660));
265    }
266    drop(file);
267}
268
269/// Puts the connection in WAL mode, retrying while SQLite answers busy, with a short growing pause.
270async fn wal(conn: &mut PoolConnection<Sqlite>) -> Result<(), sqlx::Error> {
271    let mut waited = Duration::ZERO;
272    let mut attempt = 0u32;
273    loop {
274        match exec(conn, "PRAGMA journal_mode = WAL").await {
275            Err(err) if is_busy(&err) && waited < BUSY_RETRY => {
276                let pause = Duration::from_millis(10u64 << attempt.min(10))
277                    .min(Duration::from_millis(200))
278                    .min(BUSY_RETRY - waited);
279                tokio::time::sleep(pause).await;
280                waited += pause;
281                attempt += 1;
282            }
283            other => return other,
284        }
285    }
286}