Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ impl Context {

/// Changes encrypted database passphrase.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
self.sql.change_passphrase(passphrase).await?;
self.sql.change_passphrase(self, passphrase).await?;
Ok(())
}

Expand Down
7 changes: 6 additions & 1 deletion src/scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use crate::location;
use crate::log::{LogExt, error, info, warn};
use crate::message::MsgId;
use crate::smtp::{Smtp, send_smtp_messages};
use crate::sql;
use crate::sql::{self, Sql};
use crate::tools::{self, duration_to_str, maybe_add_time_based_warnings, time, time_elapsed};

pub(crate) mod connectivity;
Expand Down Expand Up @@ -506,6 +506,11 @@ async fn inbox_fetch_idle(ctx: &Context, imap: &mut Imap, mut session: Session)
last_housekeeping_time.saturating_add(constants::HOUSEKEEPING_PERIOD);
if next_housekeeping_time <= time() {
sql::housekeeping(ctx).await.log_err(ctx).ok();
} else {
let force_truncate = false;
if let Err(err) = Sql::wal_checkpoint(ctx, force_truncate).await {
warn!(ctx, "wal_checkpoint() failed: {err:#}.");
}
}
}
Err(err) => {
Expand Down
34 changes: 28 additions & 6 deletions src/sql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,12 @@ impl Sql {
info!(context, "Opened database {:?}.", self.dbfile);
*self.is_encrypted.write().await = Some(passphrase_nonempty);

// Some migrations want housekeeping to run. Also if housekeeping failed before, fixing the
// reason and restarting the program is the most natural way to retry it.
context
.set_config_internal(Config::LastHousekeeping, None)
.await?;

// setup debug logging if there is an entry containing its id
if let Some(xdc_id) = self
.get_raw_config_u32(Config::DebugLogging.as_ref())
Expand All @@ -292,7 +298,11 @@ impl Sql {
/// The database must already be encrypted and the passphrase cannot be empty.
/// It is impossible to turn encrypted database into unencrypted
/// and vice versa this way, use import/export for this.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
pub(crate) async fn change_passphrase(
&self,
_context: &Context,
passphrase: String,
) -> Result<()> {
let mut lock = self.pool.write().await;

let pool = lock.take().context("SQL connection pool is not open")?;
Expand Down Expand Up @@ -640,8 +650,12 @@ impl Sql {
&self.config_cache
}

/// Runs a checkpoint operation in TRUNCATE mode, so the WAL file is truncated to 0 bytes.
pub(crate) async fn wal_checkpoint(context: &Context) -> Result<()> {
/// Runs a WAL checkpoint operation.
///
/// * `force_truncate` - Force TRUNCATE mode to truncate the WAL file to 0 bytes, otherwise only
/// run PASSIVE mode if the WAL isn't too large. NB: Truncating blocks all db connections for
/// some time.
pub(crate) async fn wal_checkpoint(context: &Context, force_truncate: bool) -> Result<()> {
let t_start = Time::now();
let lock = context.sql.pool.read().await;
let Some(pool) = lock.as_ref() else {
Expand All @@ -652,13 +666,19 @@ impl Sql {
// Do as much work as possible without blocking anybody.
let query_only = true;
let conn = pool.get(query_only).await?;
tokio::task::block_in_place(|| {
let pages_total = tokio::task::block_in_place(|| {
// Execute some transaction causing the WAL file to be opened so that the
// `wal_checkpoint()` can proceed, otherwise it fails when called the first time,
// see https://sqlite.org/forum/forumpost/7512d76a05268fc8.
conn.query_row("PRAGMA table_list", [], |_| Ok(()))?;
conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |_| Ok(()))
conn.query_row("PRAGMA wal_checkpoint(PASSIVE)", [], |row| {
let pages_total: i64 = row.get(1)?;
Ok(pages_total)
})
})?;
if !force_truncate && pages_total < 4096 {
return Ok(());
}

// Kick out writers.
const _: () = assert!(Sql::N_DB_CONNECTIONS > 1, "Deadlock possible");
Expand Down Expand Up @@ -729,6 +749,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
PRAGMA busy_timeout = 0; -- fail immediately
PRAGMA soft_heap_limit = 8388608; -- 8 MiB limit, same as set in Android SQLiteDatabase.
PRAGMA foreign_keys=on;
PRAGMA wal_autocheckpoint=N;
",
)?;

Expand Down Expand Up @@ -837,7 +858,8 @@ pub async fn housekeeping(context: &Context) -> Result<()> {
// bigger than 200M) and also make sure we truncate the WAL periodically. Auto-checkponting does
// not normally truncate the WAL (unless the `journal_size_limit` pragma is set), see
// https://www.sqlite.org/wal.html.
if let Err(err) = Sql::wal_checkpoint(context).await {
let force_truncate = true;
if let Err(err) = Sql::wal_checkpoint(context, force_truncate).await {
warn!(context, "wal_checkpoint() failed: {err:#}.");
debug_assert!(false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/sql/sql_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ async fn test_sql_change_passphrase() -> Result<()> {
sql.open(&t, "foo".to_string())
.await
.context("failed to open the database second time")?;
sql.change_passphrase("bar".to_string())
sql.change_passphrase(&t, "bar".to_string())
.await
.context("failed to change passphrase")?;

Expand Down
Loading