Skip to main content

rusqlite/
busy.rs

1//! Busy handler (when the database is locked)
2use std::ffi::{c_int, c_void};
3use std::mem;
4use std::panic::catch_unwind;
5use std::ptr;
6use std::time::Duration;
7
8use crate::ffi;
9use crate::{Connection, InnerConnection, Result};
10
11impl Connection {
12    /// Set a busy handler that sleeps for a specified amount of time when a
13    /// table is locked. The handler will sleep multiple times until at
14    /// least "ms" milliseconds of sleeping have accumulated.
15    ///
16    /// Calling this routine with an argument equal to zero turns off all busy
17    /// handlers.
18    ///
19    /// There can only be a single busy handler for a particular database
20    /// connection at any given moment. If another busy handler was defined
21    /// (using [`busy_handler`](Connection::busy_handler)) prior to calling this
22    /// routine, that other busy handler is cleared.
23    ///
24    /// Newly created connections currently have a default busy timeout of
25    /// 5000ms, but this may be subject to change.
26    pub fn busy_timeout(&self, timeout: Duration) -> Result<()> {
27        let ms: i32 = timeout
28            .as_secs()
29            .checked_mul(1000)
30            .and_then(|t| t.checked_add(timeout.subsec_millis().into()))
31            .and_then(|t| t.try_into().ok())
32            .expect("too big");
33        self.db.borrow_mut().busy_timeout(ms)
34    }
35
36    /// Register a callback to handle `SQLITE_BUSY` errors.
37    ///
38    /// If the busy callback is `None`, then `SQLITE_BUSY` is returned
39    /// immediately upon encountering the lock. The argument to the busy
40    /// handler callback is the number of times that the
41    /// busy handler has been invoked previously for the
42    /// same locking event. If the busy callback returns `false`, then no
43    /// additional attempts are made to access the
44    /// database and `SQLITE_BUSY` is returned to the
45    /// application. If the callback returns `true`, then another attempt
46    /// is made to access the database and the cycle repeats.
47    ///
48    /// There can only be a single busy handler defined for each database
49    /// connection. Setting a new busy handler clears any previously set
50    /// handler. Note that calling [`busy_timeout()`](Connection::busy_timeout)
51    /// or evaluating `PRAGMA busy_timeout=N` will change the busy handler
52    /// and thus clear any previously set busy handler.
53    ///
54    /// Newly created connections default to a
55    /// [`busy_timeout()`](Connection::busy_timeout) handler with a timeout
56    /// of 5000ms, although this is subject to change.
57    pub fn busy_handler(&self, callback: Option<fn(i32) -> bool>) -> Result<()> {
58        unsafe extern "C" fn busy_handler_callback(p_arg: *mut c_void, count: c_int) -> c_int {
59            let handler_fn: fn(i32) -> bool = mem::transmute(p_arg);
60            c_int::from(catch_unwind(|| handler_fn(count)).unwrap_or_default())
61        }
62        let c = self.db.borrow_mut();
63        c.decode_result(unsafe {
64            ffi::sqlite3_busy_handler(
65                c.db(),
66                callback.as_ref().map(|_| busy_handler_callback as _),
67                callback.map_or_else(ptr::null_mut, |f| f as *mut c_void),
68            )
69        })
70    }
71}
72
73impl InnerConnection {
74    #[inline]
75    fn busy_timeout(&mut self, timeout: c_int) -> Result<()> {
76        let r = unsafe { ffi::sqlite3_busy_timeout(self.db, timeout) };
77        self.decode_result(r)
78    }
79}
80
81#[cfg(test)]
82mod test {
83    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
84    use wasm_bindgen_test::wasm_bindgen_test as test;
85
86    use crate::{Connection, ErrorCode, Result, TransactionBehavior};
87    use std::sync::atomic::{AtomicBool, Ordering};
88
89    #[cfg_attr(
90        all(target_family = "wasm", target_os = "unknown"),
91        ignore = "no filesystem on this platform"
92    )]
93    #[test]
94    fn test_default_busy() -> Result<()> {
95        let temp_dir = tempfile::tempdir().unwrap();
96        let path = temp_dir.path().join("test.db3");
97
98        let mut db1 = Connection::open(&path)?;
99        let tx1 = db1.transaction_with_behavior(TransactionBehavior::Exclusive)?;
100        let db2 = Connection::open(&path)?;
101        let r: Result<()> = db2.query_row("PRAGMA schema_version", [], |_| unreachable!());
102        assert_eq!(
103            r.unwrap_err().sqlite_error_code(),
104            Some(ErrorCode::DatabaseBusy)
105        );
106        tx1.rollback()
107    }
108
109    #[cfg_attr(
110        all(target_family = "wasm", target_os = "unknown"),
111        ignore = "no filesystem on this platform"
112    )]
113    #[test]
114    fn test_busy_handler() -> Result<()> {
115        static CALLED: AtomicBool = AtomicBool::new(false);
116        fn busy_handler(n: i32) -> bool {
117            if n > 2 {
118                false
119            } else {
120                CALLED.swap(true, Ordering::Relaxed)
121            }
122        }
123
124        let temp_dir = tempfile::tempdir().unwrap();
125        let path = temp_dir.path().join("busy-handler.db3");
126
127        let db1 = Connection::open(&path)?;
128        db1.execute_batch("CREATE TABLE IF NOT EXISTS t(a)")?;
129        let db2 = Connection::open(&path)?;
130        db2.busy_handler(Some(busy_handler))?;
131        db1.execute_batch("BEGIN EXCLUSIVE")?;
132        let err = db2.prepare("SELECT * FROM t").unwrap_err();
133        assert_eq!(err.sqlite_error_code(), Some(ErrorCode::DatabaseBusy));
134        assert!(CALLED.load(Ordering::Relaxed));
135        db1.busy_handler(None)?;
136        Ok(())
137    }
138}