Skip to main content

ouisync/db/
mod.rs

1#[macro_use]
2mod macros;
3
4mod connection;
5mod id;
6mod migrations;
7
8pub use id::DatabaseId;
9pub use migrations::SCHEMA_VERSION;
10
11use tracing::Span;
12
13use deadlock::ExpectShortLifetime;
14use ref_cast::RefCast;
15use sqlx::{
16    ConnectOptions, Row, SqlitePool, TransactionManager,
17    sqlite::{
18        Sqlite, SqliteAutoVacuum, SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions,
19        SqliteSynchronous, SqliteTransactionManager,
20    },
21};
22use std::{
23    fmt, io,
24    ops::{Deref, DerefMut},
25    panic::Location,
26    path::{Path, PathBuf},
27    time::Duration,
28};
29#[cfg(test)]
30use tempfile::TempDir;
31use thiserror::Error;
32use tokio::{fs, sync::Semaphore, task};
33
34const ACQUIRE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
35const IDLE_TIMEOUT: Duration = Duration::from_secs(60);
36const WARN_AFTER_CONNECTION_LIFETIME: Duration = Duration::from_secs(30);
37
38pub use self::connection::Connection;
39
40/// Database connection pool.
41#[derive(Clone)]
42pub struct Pool {
43    // Path to the main database file
44    path: PathBuf,
45    // Pool with multiple read-only connections
46    reads: SqlitePool,
47    // Pool with a single writable connection.
48    write: SqlitePool,
49}
50
51impl Pool {
52    async fn create(conn_options: SqliteConnectOptions) -> Result<Self, sqlx::Error> {
53        let path = conn_options.get_filename().to_path_buf();
54
55        if fs::try_exists(&path).await.unwrap_or(false) {
56            // Try to enable auto-vacuum, but if it fails [^1] it's not a critical failure as we can
57            // keep using the db without auto-vacuum [^2]. Just log the error and keep going.
58            //
59            // [^1]: for example, because of low disk space
60            // [^2]: with the downside that disk space won't be reclaimed after data deletion
61            if let Err(error) = enable_auto_vacuum(conn_options.get_filename()).await {
62                tracing::warn!(?error, "failed to enable auto vacuum");
63            }
64        }
65
66        let conn_options = conn_options
67            .journal_mode(SqliteJournalMode::Wal)
68            .synchronous(SqliteSynchronous::Normal)
69            .pragma("recursive_triggers", "ON");
70
71        let pool_options = SqlitePoolOptions::new()
72            // Disable the test as it breaks cancel-safety (also it's unnecessary in our case)
73            .test_before_acquire(false)
74            // Expire idle connections to conserve resources (threads, file descriptors)
75            .idle_timeout(IDLE_TIMEOUT)
76            .acquire_timeout(ACQUIRE_TIMEOUT);
77
78        let write = pool_options
79            .clone()
80            .max_connections(1)
81            .connect_with(
82                conn_options
83                    .clone()
84                    .auto_vacuum(SqliteAutoVacuum::Full)
85                    .optimize_on_close(true, Some(1000)),
86            )
87            .await?;
88
89        let reads = pool_options
90            .max_connections(8)
91            .connect_with(conn_options.read_only(true))
92            .await?;
93
94        Ok(Self { path, reads, write })
95    }
96
97    /// Return path to the database file.
98    pub fn store_path(&self) -> &Path {
99        self.path.as_ref()
100    }
101
102    /// Acquire a read-only database connection.
103    #[track_caller]
104    pub fn acquire(&self) -> impl Future<Output = Result<PoolConnection, sqlx::Error>> + '_ {
105        PoolConnection::acquire(&self.reads, Location::caller())
106    }
107
108    /// Begin a read-only transaction. See [`ReadTransaction`] for more details.
109    #[track_caller]
110    pub fn begin_read(&self) -> impl Future<Output = Result<ReadTransaction, sqlx::Error>> + '_ {
111        ReadTransaction::begin(&self.reads, Location::caller())
112    }
113
114    /// Begin a write transaction. See [`WriteTransaction`] for more details.
115    #[track_caller]
116    pub fn begin_write(&self) -> impl Future<Output = Result<WriteTransaction, sqlx::Error>> + '_ {
117        let location = Location::caller();
118
119        async move {
120            Ok(WriteTransaction {
121                inner: ReadTransaction::begin(&self.write, location).await?,
122            })
123        }
124    }
125
126    pub(crate) async fn close(&self) -> Result<(), sqlx::Error> {
127        // Make sure to first close `reads` and only then `write`. That way when closing the write
128        // connection it is the last remaining connection and so it performs a WAL checkpoint and
129        // removes the auxiliary db files (*-wal and *-shm).
130        self.reads.close().await;
131        self.write.close().await;
132
133        Ok(())
134    }
135}
136
137/// Database connection from pool
138pub struct PoolConnection {
139    inner: sqlx::pool::PoolConnection<Sqlite>,
140    _track_lifetime: ExpectShortLifetime,
141}
142
143impl PoolConnection {
144    // Internal
145    async fn acquire(
146        pool: &SqlitePool,
147        location: &'static Location<'static>,
148    ) -> Result<Self, sqlx::Error> {
149        let inner = pool.acquire().await?;
150        let track_lifetime = ExpectShortLifetime::new_in(WARN_AFTER_CONNECTION_LIFETIME, location);
151
152        Ok(Self {
153            inner,
154            _track_lifetime: track_lifetime,
155        })
156    }
157}
158
159impl Deref for PoolConnection {
160    type Target = Connection;
161
162    fn deref(&self) -> &Self::Target {
163        Connection::ref_cast(self.inner.deref())
164    }
165}
166
167impl DerefMut for PoolConnection {
168    fn deref_mut(&mut self) -> &mut Self::Target {
169        Connection::ref_cast_mut(self.inner.deref_mut())
170    }
171}
172
173/// Transaction that allows only reading.
174///
175/// This is useful if one wants to make sure the observed database content doesn't change for the
176/// duration of the transaction even in the presence of concurrent writes. In other words - a read
177/// transaction represents an immutable snapshot of the database at the point the transaction was
178/// created. A read transaction doesn't need to be committed or rolled back - it's implicitly ended
179/// when the `ReadTransaction` instance drops.
180pub struct ReadTransaction {
181    inner: PoolConnection,
182    closed: bool,
183}
184
185impl ReadTransaction {
186    // Internal
187    async fn begin(
188        pool: &SqlitePool,
189        location: &'static Location<'static>,
190    ) -> Result<Self, sqlx::Error> {
191        let mut inner = PoolConnection::acquire(pool, location).await?;
192        SqliteTransactionManager::begin(&mut inner.inner, None).await?;
193
194        Ok(Self {
195            inner,
196            closed: false,
197        })
198    }
199
200    // Internal
201    async fn commit(mut self) -> Result<Committed, sqlx::Error> {
202        SqliteTransactionManager::commit(&mut self.inner.inner).await?;
203        self.closed = true;
204        Ok(Committed(self))
205    }
206}
207
208impl Deref for ReadTransaction {
209    type Target = Connection;
210
211    fn deref(&self) -> &Self::Target {
212        self.inner.deref()
213    }
214}
215
216impl DerefMut for ReadTransaction {
217    fn deref_mut(&mut self) -> &mut Self::Target {
218        self.inner.deref_mut()
219    }
220}
221
222impl fmt::Debug for ReadTransaction {
223    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
224        f.debug_struct("ReadTransaction").finish_non_exhaustive()
225    }
226}
227
228impl_executor_by_deref!(ReadTransaction);
229
230impl Drop for ReadTransaction {
231    fn drop(&mut self) {
232        if !self.closed {
233            SqliteTransactionManager::start_rollback(&mut self.inner.inner);
234        }
235    }
236}
237
238// Wrapper for a transaction that's been committed. This allows to delay releasing the underlying
239// connection to the pool while disallowing using the connection to execute any db operations.
240struct Committed(#[allow(dead_code)] ReadTransaction);
241
242/// Transaction that allows both reading and writing.
243///
244/// At most one task can hold a write transaction at any time. Any other tasks are blocked on
245/// calling `begin_write` until the task that currently holds it is done with it (commits it or
246/// rolls it back). Performing read-only operations concurrently while a write transaction is in
247/// use is still allowed. Those operations will not see the writes performed via the write
248/// transaction until that transaction is committed however.
249pub struct WriteTransaction {
250    inner: ReadTransaction,
251}
252
253impl WriteTransaction {
254    /// Commits the transaction.
255    ///
256    /// # Cancel safety
257    ///
258    /// If the future returned by this function is cancelled before completion, the transaction
259    /// is guaranteed to be either committed or rolled back but there is no way to tell in advance
260    /// which of the two operations happens.
261    pub async fn commit(self) -> Result<(), sqlx::Error> {
262        self.inner.commit().await?;
263        Ok(())
264    }
265
266    /// Commits the transaction and if (and only if) the commit completes successfully, runs the
267    /// given closure.
268    ///
269    /// # Atomicity
270    ///
271    /// If the commit succeeds, the closure is guaranteed to complete before another write
272    /// transaction begins.
273    ///
274    /// # Cancel safety
275    ///
276    /// The commits completes and if it succeeds the closure gets called. This is guaranteed to
277    /// happen even if the future returned from this function is cancelled before completion.
278    ///
279    /// # Insufficient alternatives
280    ///
281    /// ## Calling `commit().await?` and then calling `f()`
282    ///
283    /// This is not enough because it has these possible outcomes depending on whether and when
284    /// cancellation happened:
285    ///
286    /// 1. `commit` completes successfully and `f` is called
287    /// 2. `commit` completes with error and `f` is not called
288    /// 3. `commit` is cancelled but the transaction is still committed and `f` is not called
289    /// 4. `commit` is cancelled and the transaction rolls back and `f` is not called
290    ///
291    /// Number 3 is typically not desirable.
292    ///
293    /// ## Calling `f` using a RAII guard
294    ///
295    /// This is still not enough because it has the following possible outcomes:
296    ///
297    /// 1. `commit` completes successfully and `f` is called
298    /// 2. `commit` completes with error and `f` is called
299    /// 3. `commit` is cancelled but the transaction is still committed and `f` is called
300    /// 4. `commit` is cancelled and the transaction rolls back and `f` is called
301    ///
302    /// Numbers 2 and 4 are not desirable. Number 2 can be handled by explicitly handling the error
303    /// case and disabling the guard but there is nothing to do about number 4.
304    pub async fn commit_and_then<F, R>(self, f: F) -> Result<R, sqlx::Error>
305    where
306        F: FnOnce() -> R + Send + 'static,
307        R: Send + 'static,
308    {
309        let span = Span::current();
310
311        task::spawn(async move {
312            // IMPORTANT: `_committed` must live until `f` completes.
313            let _committed = self.inner.commit().await?;
314            let result = span.in_scope(f);
315            Ok(result)
316        })
317        .await
318        .unwrap()
319    }
320}
321
322impl Deref for WriteTransaction {
323    type Target = ReadTransaction;
324
325    fn deref(&self) -> &Self::Target {
326        &self.inner
327    }
328}
329
330impl DerefMut for WriteTransaction {
331    fn deref_mut(&mut self) -> &mut Self::Target {
332        &mut self.inner
333    }
334}
335
336impl std::fmt::Debug for WriteTransaction {
337    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
338        write!(f, "WriteTransaction{{ inner:{:?} }}", self.inner)
339    }
340}
341
342impl_executor_by_deref!(WriteTransaction);
343
344#[derive(Debug, Error)]
345pub enum Error {
346    #[error("failed to create database directory")]
347    CreateDirectory(#[source] io::Error),
348    #[error("database already exists")]
349    Exists,
350    #[error("failed to open database")]
351    Open(#[source] sqlx::Error),
352    #[error("failed to execute database query")]
353    Query(#[from] sqlx::Error),
354}
355
356/// Creates a new database and opens a connection to it.
357pub(crate) async fn create(path: impl AsRef<Path>) -> Result<Pool, Error> {
358    let path = path.as_ref();
359
360    if fs::metadata(path).await.is_ok() {
361        return Err(Error::Exists);
362    }
363
364    create_directory(path).await?;
365
366    let connect_options = SqliteConnectOptions::new()
367        .filename(path)
368        .create_if_missing(true);
369
370    let pool = Pool::create(connect_options).await.map_err(Error::Open)?;
371
372    migrations::run(&pool).await?;
373
374    Ok(pool)
375}
376
377/// Creates a new database in a temporary directory. Useful for tests.
378#[cfg(test)]
379pub(crate) async fn create_temp() -> Result<(TempDir, Pool), Error> {
380    let temp_dir = TempDir::new().map_err(Error::CreateDirectory)?;
381    let pool = create(temp_dir.path().join("temp.db")).await?;
382
383    Ok((temp_dir, pool))
384}
385
386/// Opens a connection to the specified database. Fails if the db doesn't exist.
387pub(crate) async fn open(path: impl AsRef<Path>) -> Result<Pool, Error> {
388    let connect_options = SqliteConnectOptions::new().filename(path);
389    let pool = Pool::create(connect_options).await.map_err(Error::Open)?;
390
391    migrations::run(&pool).await?;
392
393    Ok(pool)
394}
395
396/// Opens a connection to the specified database. Fails if the db doesn't exist.
397pub async fn open_without_migrations(path: impl AsRef<Path>) -> Result<Pool, Error> {
398    let connect_options = SqliteConnectOptions::new().filename(path);
399    let pool = Pool::create(connect_options).await.map_err(Error::Open)?;
400
401    Ok(pool)
402}
403
404async fn create_directory(path: &Path) -> Result<(), Error> {
405    if let Some(dir) = path.parent() {
406        fs::create_dir_all(dir)
407            .await
408            .map_err(Error::CreateDirectory)?
409    }
410
411    Ok(())
412}
413
414// Explicit cast from `i64` to `u64` to work around the lack of native `u64` support in the sqlx
415// crate.
416pub(crate) const fn decode_u64(i: i64) -> u64 {
417    i as u64
418}
419
420// Explicit cast from `u64` to `i64` to work around the lack of native `u64` support in the sqlx
421// crate.
422pub(crate) const fn encode_u64(u: u64) -> i64 {
423    u as i64
424}
425
426// Enable auto-vacuum on the given database unless already enabled
427async fn enable_auto_vacuum(db_path: &Path) -> Result<(), Error> {
428    let mut conn = SqliteConnectOptions::new()
429        .filename(db_path)
430        .connect()
431        .await?;
432
433    let auto_vacuum: u32 = sqlx::query("PRAGMA auto_vacuum")
434        .fetch_one(&mut conn)
435        .await?
436        .get(0);
437
438    if auto_vacuum != 0 {
439        return Ok(());
440    }
441
442    // VACUUM requires up to twice the size of the original database of free disk space. To prevent
443    // exhausting available disk space when opening multiple databases in parallel, we limit the
444    // number of dbs being vacuumed at the same time to one. Dbs that don't need to be vacuumed are
445    // not affected by this limit.
446    static SEMAPHORE: Semaphore = Semaphore::const_new(1);
447    let _permit = SEMAPHORE.acquire().await.unwrap();
448
449    sqlx::query("PRAGMA auto_vacuum=FULL")
450        .execute(&mut conn)
451        .await?;
452
453    // Execute `VACUUM` command for the `auto_vacuum` pragma to take effect.
454    sqlx::query("VACUUM").execute(&mut conn).await?;
455
456    Ok(())
457}
458
459async fn get_pragma(conn: &mut Connection, name: &str) -> Result<u32, Error> {
460    Ok(sqlx::query(&format!("PRAGMA {name}"))
461        .fetch_one(&mut *conn)
462        .await?
463        .get(0))
464}
465
466async fn set_pragma(conn: &mut Connection, name: &str, value: u32) -> Result<(), Error> {
467    // `bind` doesn't seem to be supported for setting PRAGMAs...
468    sqlx::query(&format!("PRAGMA {name} = {value}"))
469        .execute(&mut *conn)
470        .await?;
471
472    Ok(())
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    // Check the casts are lossless
480
481    #[test]
482    fn decode_u64_sanity_check() {
483        // [0i64,     i64::MAX] -> [0u64,             u64::MAX / 2]
484        // [i64::MIN,    -1i64] -> [u64::MAX / 2 + 1,     u64::MAX]
485
486        assert_eq!(decode_u64(0), 0);
487        assert_eq!(decode_u64(1), 1);
488        assert_eq!(decode_u64(-1), u64::MAX);
489        assert_eq!(decode_u64(i64::MIN), u64::MAX / 2 + 1);
490        assert_eq!(decode_u64(i64::MAX), u64::MAX / 2);
491    }
492
493    #[test]
494    fn encode_u64_sanity_check() {
495        assert_eq!(encode_u64(0), 0);
496        assert_eq!(encode_u64(1), 1);
497        assert_eq!(encode_u64(u64::MAX / 2), i64::MAX);
498        assert_eq!(encode_u64(u64::MAX / 2 + 1), i64::MIN);
499        assert_eq!(encode_u64(u64::MAX), -1);
500    }
501}