rusqlite 0.41.0

Ergonomic wrapper for SQLite
Documentation
//! Busy handler (when the database is locked)
use std::ffi::{c_int, c_void};
use std::panic::catch_unwind;
use std::time::Duration;

use crate::ffi;
use crate::{Connection, InnerConnection, Result};

impl Connection {
    /// Set a busy handler that sleeps for a specified amount of time when a
    /// table is locked. The handler will sleep multiple times until at
    /// least "ms" milliseconds of sleeping have accumulated.
    ///
    /// Calling this routine with an argument equal to zero turns off all busy
    /// handlers.
    ///
    /// There can only be a single busy handler for a particular database
    /// connection at any given moment. If another busy handler was defined
    /// (using [`busy_handler`](Connection::busy_handler)) prior to calling this
    /// routine, that other busy handler is cleared.
    ///
    /// Newly created connections currently have a default busy timeout of
    /// 5000ms, but this may be subject to change.
    pub fn busy_timeout(&self, timeout: Duration) -> Result<()> {
        let ms: i32 = timeout
            .as_secs()
            .checked_mul(1000)
            .and_then(|t| t.checked_add(timeout.subsec_millis().into()))
            .and_then(|t| t.try_into().ok())
            .expect("too big");
        self.db.borrow_mut().busy_timeout(ms)
    }

    /// Register a callback to handle `SQLITE_BUSY` errors.
    ///
    /// If the busy callback is `None`, then `SQLITE_BUSY` is returned
    /// immediately upon encountering the lock. The argument to the busy
    /// handler callback is the number of times that the
    /// busy handler has been invoked previously for the
    /// same locking event. If the busy callback returns `false`, then no
    /// additional attempts are made to access the
    /// database and `SQLITE_BUSY` is returned to the
    /// application. If the callback returns `true`, then another attempt
    /// is made to access the database and the cycle repeats.
    ///
    /// There can only be a single busy handler defined for each database
    /// connection. Setting a new busy handler clears any previously set
    /// handler. Note that calling [`busy_timeout()`](Connection::busy_timeout)
    /// or evaluating `PRAGMA busy_timeout=N` will change the busy handler
    /// and thus clear any previously set busy handler.
    ///
    /// Newly created connections default to a
    /// [`busy_timeout()`](Connection::busy_timeout) handler with a timeout
    /// of 5000ms, although this is subject to change.
    pub fn busy_handler<F>(&mut self, callback: Option<F>) -> Result<()>
    where
        F: FnMut(i32) -> bool + Send + 'static,
    {
        unsafe extern "C" fn busy_handler_callback<F>(p_arg: *mut c_void, count: c_int) -> c_int
        where
            F: FnMut(i32) -> bool,
        {
            unsafe {
                c_int::from(
                    catch_unwind(|| {
                        let handler_fn = p_arg.cast::<F>();
                        (*handler_fn)(count)
                    })
                    .unwrap_or_default(),
                )
            }
        }
        let x = callback.as_ref().map(|_| busy_handler_callback::<F> as _);
        let mut c = self.db.borrow_mut();
        c.set_clientdata(c"sqlite3_busy_handler", callback, |db, bh| unsafe {
            ffi::sqlite3_busy_handler(db, x, bh)
        })?;
        Ok(())
    }
}

impl InnerConnection {
    #[inline]
    fn busy_timeout(&mut self, timeout: c_int) -> Result<()> {
        let r = unsafe { ffi::sqlite3_busy_timeout(self.db, timeout) };
        let res = self.decode_result(r);
        if res.is_ok() {
            self.set_clientdata(c"sqlite3_busy_handler", None::<c_void>, |_, _| {
                ffi::SQLITE_OK
            })?;
        }
        res
    }
}

#[cfg(all(test, not(miri)))]
mod test {
    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
    use wasm_bindgen_test::wasm_bindgen_test as test;

    use crate::{Connection, ErrorCode, Result, TransactionBehavior};
    use std::sync::atomic::{AtomicBool, Ordering};

    #[cfg_attr(
        all(target_family = "wasm", target_os = "unknown"),
        ignore = "no filesystem on this platform"
    )]
    #[test]
    fn test_default_busy() -> Result<()> {
        let temp_dir = tempfile::tempdir().unwrap();
        let path = temp_dir.path().join("test.db3");

        let mut db1 = Connection::open(&path)?;
        let tx1 = db1.transaction_with_behavior(TransactionBehavior::Exclusive)?;
        let db2 = Connection::open(&path)?;
        let r: Result<()> = db2.query_row("PRAGMA schema_version", [], |_| unreachable!());
        assert_eq!(
            r.unwrap_err().sqlite_error_code(),
            Some(ErrorCode::DatabaseBusy)
        );
        tx1.rollback()
    }

    #[cfg_attr(
        all(target_family = "wasm", target_os = "unknown"),
        ignore = "no filesystem on this platform"
    )]
    #[test]
    fn test_busy_handler() -> Result<()> {
        static CALLED: AtomicBool = AtomicBool::new(false);
        fn busy_handler(n: i32) -> bool {
            if n > 2 {
                false
            } else {
                CALLED.swap(true, Ordering::Relaxed)
            }
        }

        let temp_dir = tempfile::tempdir().unwrap();
        let path = temp_dir.path().join("busy-handler.db3");

        let mut db1 = Connection::open(&path)?;
        db1.execute_batch("CREATE TABLE IF NOT EXISTS t(a)")?;
        let mut db2 = Connection::open(&path)?;
        db2.busy_handler(Some(busy_handler))?;
        db1.execute_batch("BEGIN EXCLUSIVE")?;
        let err = db2.prepare("SELECT * FROM t").unwrap_err();
        assert_eq!(err.sqlite_error_code(), Some(ErrorCode::DatabaseBusy));
        assert!(CALLED.load(Ordering::Relaxed));
        db2.busy_handler(None::<fn(i32) -> bool>)?;

        db1.busy_handler(Some(busy_handler))?;
        db1.busy_timeout(std::time::Duration::from_millis(5))?;
        assert!(
            unsafe { db1.get_clientdata::<std::ffi::c_void, _>(c"sqlite3_busy_handler")? }
                .is_none()
        );
        Ok(())
    }
}