diff options
| -rw-r--r-- | Cargo.lock | 28 | ||||
| -rw-r--r-- | Cargo.toml | 10 | ||||
| -rw-r--r-- | src/db/backup.rs | 134 |
3 files changed, 65 insertions, 107 deletions
@@ -555,6 +555,18 @@ dependencies = [ ] [[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] name = "fastrand" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -795,10 +807,10 @@ dependencies = [ "headers", "hex-literal", "itertools", - "libsqlite3-sys", "password-hash", "rand", "rand_core", + "rusqlite", "serde", "serde_json", "sqlx", @@ -1369,6 +1381,20 @@ dependencies = [ ] [[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -14,16 +14,16 @@ futures = "0.3.30" headers = "0.4.0" hex-literal = "0.4.1" itertools = "0.13.0" -# Pinned to keep sqlx and libsqlite3 in lockstep. See also: -# <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html> -libsqlite3-sys = { version = "=0.30.1", features = ["bundled"] } password-hash = { version = "0.5.0", features = ["std"] } rand = "0.8.5" rand_core = { version = "0.6.4", features = ["getrandom"] } +# Pinned to maintain libsqlite3 version match between this and sqlx. See also: +# <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html> +rusqlite = { version = "=0.32.1", features = ["backup"] } serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" -# Pinned to keep sqlx and libsqlite3 in lockstep. See also: -# <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html> +# Pinned to maintain libsqlite3 version match between this and rusqlite. See +# also: <https://docs.rs/sqlx/latest/sqlx/sqlite/index.html> sqlx = { version = "=0.8.2", features = ["chrono", "runtime-tokio", "sqlite"] } thiserror = "1.0.64" tokio = { version = "1.40.0", features = ["rt", "macros", "rt-multi-thread"] } diff --git a/src/db/backup.rs b/src/db/backup.rs index b09bb3d..bb36aea 100644 --- a/src/db/backup.rs +++ b/src/db/backup.rs @@ -1,14 +1,8 @@ -use std::{ - ffi::{c_int, CStr, CString}, - ptr::NonNull, - str::from_utf8, +use rusqlite::{ + backup::{self}, + Connection, }; - -use libsqlite3_sys::{ - sqlite3, sqlite3_backup, sqlite3_backup_finish, sqlite3_backup_init, sqlite3_backup_step, - sqlite3_errmsg, sqlite3_errstr, sqlite3_extended_errcode, SQLITE_DONE, SQLITE_OK, -}; -use sqlx::sqlite::SqlitePool; +use sqlx::sqlite::{LockedSqliteHandle, SqlitePool}; pub struct Builder<'p> { from: &'p SqlitePool, @@ -37,63 +31,39 @@ impl<'p> Backup<'p> { impl<'p> Backup<'p> { pub async fn backup(&mut self) -> Result<(), Error> { let mut to = self.to.acquire().await?; - let mut to = to.lock_handle().await?; + let mut to = Self::connection(&mut to.lock_handle().await?)?; let mut from = self.from.acquire().await?; - let mut from = from.lock_handle().await?; - - let handle = Self::start(to.as_raw_handle(), from.as_raw_handle())?; - let step_result = loop { - match Self::step(handle, -1) { - Err(error) => break Err(error), - Ok(SQLITE_DONE) => break Ok(()), - Ok(SQLITE_OK) => (), // keep pumping the backup step function - Ok(other) => panic!("unexpected step result: {other}"), + let from = Self::connection(&mut from.lock_handle().await?)?; + + let backup = backup::Backup::new(&from, &mut to)?; + loop { + use rusqlite::backup::StepResult::{Busy, Done, Locked, More}; + match backup.step(-1)? { + Done => break Ok(()), + // In the system as actually written, yielding does nothing: this is the + // only task actually in flight at the time. However, that may change; if + // it does, we _definitely_ want to give other tasks an opportunity to + // make progress before we retry. + More => tokio::task::consume_budget().await, + // If another task is actually using the DB, then it _definitely_ needs + // an opportunity to make progress before we retry. + Busy | Locked => tokio::task::yield_now().await, + // As of this writing, this arm is formally unreachable: all of the enum + // constants that actually exist in rusqlite are matched above. However, + // they've reserved the prerogative to add additional arms without a + // breaking change by making the type as non-exhaustive; if we get one, + // stop immediately as there's no way to guess what the correct handling + // will be, a priori. + other => panic!("unexpected backup step result: {other:?}"), } - }; - Self::finish(handle)?; - - step_result - } - - fn start( - to: NonNull<sqlite3>, - from: NonNull<sqlite3>, - ) -> Result<NonNull<sqlite3_backup>, Error> { - let name = CString::new("main").expect("static constant is a valid C string"); - // Invariants: - // - // * `to` and `from` must be valid `sqlite3` pointers (guaranteed by sqlx) - // * `zDestName` and `zSourceName` must be valid C strings (see above) - // - // Never evaluates to null (even though `sqlite3_backup_init` can). - let handle = unsafe { - sqlite3_backup_init(to.as_ptr(), name.as_ptr(), from.as_ptr(), name.as_ptr()) - }; - if handle.is_null() { - Err(Error::from_handle(to))?; } - // Having proven that `handle` is not null, we could use new_unchecked here. - // Choosing not to so that any mistakes are caught, rather than causing - // undefined behaviour later on. - Ok(NonNull::new(handle).expect("backup handle is non-null")) } - fn step(handle: NonNull<sqlite3_backup>, pages: c_int) -> Result<c_int, Error> { - let step = unsafe { sqlite3_backup_step(handle.as_ptr(), pages) }; - if [SQLITE_DONE, SQLITE_OK].contains(&step) { - Ok(step) - } else { - Err(Error::from_code(step)) - } - } + fn connection(handle: &mut LockedSqliteHandle) -> Result<Connection, rusqlite::Error> { + let handle = handle.as_raw_handle(); + let connection = unsafe { Connection::from_handle(handle.as_ptr()) }?; - fn finish(handle: NonNull<sqlite3_backup>) -> Result<(), Error> { - let finished = unsafe { sqlite3_backup_finish(handle.as_ptr()) }; - if SQLITE_OK == finished { - Ok(()) - } else { - Err(Error::from_code(finished)) - } + Ok(connection) } } @@ -101,44 +71,6 @@ impl<'p> Backup<'p> { pub enum Error { #[error(transparent)] Sqlx(#[from] sqlx::Error), - #[error("backup failed: {message} (code={code})")] - Backup { code: c_int, message: String }, -} - -impl Error { - pub fn from_handle(handle: NonNull<sqlite3>) -> Self { - Self::Backup { - code: Self::code_for(handle), - message: Self::message_for(handle), - } - } - - pub fn from_code(code: c_int) -> Self { - Self::Backup { - code, - message: Self::message_from_code(code), - } - } - - fn code_for(handle: NonNull<sqlite3>) -> c_int { - unsafe { sqlite3_extended_errcode(handle.as_ptr()) } - } - - fn message_for(handle: NonNull<sqlite3>) -> String { - Self::message_from(|| unsafe { sqlite3_errmsg(handle.as_ptr()) }) - } - - fn message_from_code(code: c_int) -> String { - Self::message_from(|| unsafe { sqlite3_errstr(code) }) - } - - fn message_from(f: impl FnOnce() -> *const i8) -> String { - let msg = f(); - debug_assert!(!msg.is_null()); - from_utf8(unsafe { CStr::from_ptr(msg) }.to_bytes()) - // This is actually promised in the Sqlite3 docs, but we check anyways to catch - // mistakes. See <https://www.sqlite.org/c3ref/errcode.html>. - .expect("error messages from sqlite are always utf-8") - .to_owned() - } + #[error(transparent)] + Rusqlite(#[from] rusqlite::Error), } |
