deltachat/sql/pool/
wal_checkpoint.rs

1//! # WAL checkpointing for SQLite connection pool.
2
3use anyhow::{Result, ensure};
4use std::sync::Arc;
5use std::time::Duration;
6
7use crate::sql::Sql;
8use crate::tools::{Time, time_elapsed};
9
10use super::Pool;
11
12/// Information about WAL checkpointing call for logging.
13#[derive(Debug)]
14pub(crate) struct WalCheckpointStats {
15    /// Duration of the whole WAL checkpointing.
16    pub total_duration: Duration,
17
18    /// Duration for which WAL checkpointing blocked the writers.
19    pub writers_blocked_duration: Duration,
20
21    /// Duration for which WAL checkpointing blocked the readers.
22    pub readers_blocked_duration: Duration,
23
24    /// Number of pages in WAL before truncating.
25    pub pages_total: i64,
26
27    /// Number of checkpointed WAL pages.
28    ///
29    /// It should be the same as `pages_total`
30    /// unless there are external connections to the database
31    /// that are not in the pool.
32    pub pages_checkpointed: i64,
33}
34
35/// Runs a checkpoint operation in TRUNCATE mode, so the WAL file is truncated to 0 bytes.
36pub(super) async fn wal_checkpoint(pool: &Pool) -> Result<WalCheckpointStats> {
37    let _guard = pool.inner.wal_checkpoint_mutex.lock().await;
38    let t_start = Time::now();
39
40    // Do as much work as possible without blocking anybody.
41    let query_only = true;
42    let conn = pool.get(query_only).await?;
43    tokio::task::block_in_place(|| {
44        // Execute some transaction causing the WAL file to be opened so that the
45        // `wal_checkpoint()` can proceed, otherwise it fails when called the first time,
46        // see https://sqlite.org/forum/forumpost/7512d76a05268fc8.
47        conn.query_row("PRAGMA table_list", [], |_| Ok(()))?;
48        conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |_| Ok(()))
49    })?;
50
51    // Kick out writers.
52    const _: () = assert!(Sql::N_DB_CONNECTIONS > 1, "Deadlock possible");
53    let _write_lock = Arc::clone(&pool.inner.write_mutex).lock_owned().await;
54    let t_writers_blocked = Time::now();
55    // Ensure that all readers use the most recent database snapshot (are at the end of WAL) so
56    // that `wal_checkpoint(FULL)` isn't blocked. We could use `PASSIVE` as well, but it's
57    // documented poorly, https://www.sqlite.org/pragma.html#pragma_wal_checkpoint and
58    // https://www.sqlite.org/c3ref/wal_checkpoint_v2.html don't tell how it interacts with new
59    // readers.
60    let mut read_conns = Vec::with_capacity(crate::sql::Sql::N_DB_CONNECTIONS - 1);
61    for _ in 0..(crate::sql::Sql::N_DB_CONNECTIONS - 1) {
62        read_conns.push(pool.get(query_only).await?);
63    }
64    read_conns.clear();
65    // Checkpoint the remaining WAL pages without blocking readers.
66    let (pages_total, pages_checkpointed) = tokio::task::block_in_place(|| {
67        conn.query_row("PRAGMA wal_checkpoint(FULL)", [], |row| {
68            let pages_total: i64 = row.get(1)?;
69            let pages_checkpointed: i64 = row.get(2)?;
70            Ok((pages_total, pages_checkpointed))
71        })
72    })?;
73    // Kick out readers to avoid blocking/SQLITE_BUSY.
74    for _ in 0..(crate::sql::Sql::N_DB_CONNECTIONS - 1) {
75        read_conns.push(pool.get(query_only).await?);
76    }
77    let t_readers_blocked = Time::now();
78    tokio::task::block_in_place(|| {
79        let blocked = conn.query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
80            let blocked: i64 = row.get(0)?;
81            Ok(blocked)
82        })?;
83        ensure!(blocked == 0);
84        Ok(())
85    })?;
86    Ok(WalCheckpointStats {
87        total_duration: time_elapsed(&t_start),
88        writers_blocked_duration: time_elapsed(&t_writers_blocked),
89        readers_blocked_duration: time_elapsed(&t_readers_blocked),
90        pages_total,
91        pages_checkpointed,
92    })
93}