sql_support/
lazy.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::open_database::{open_database_with_flags, ConnectionInitializer, Error};
6use interrupt_support::{register_interrupt, SqlInterruptHandle, SqlInterruptScope};
7use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
8use rusqlite::{Connection, OpenFlags};
9use std::{
10    path::{Path, PathBuf},
11    sync::{Arc, Weak},
12};
13
14/// Lazily-loaded database with interruption support
15///
16/// In addition to the [Self::interrupt] method, LazyDb also calls
17/// [interrupt_support::register_interrupt] on any opened database.  This means that if
18/// [interrupt_support::shutdown] is called it will interrupt this database if it's open and
19/// in-use.
20pub struct LazyDb<CI> {
21    path: PathBuf,
22    open_flags: OpenFlags,
23    connection_initializer: CI,
24    // Note: if you're going to lock both mutexes at once, make sure to lock the connection mutex
25    // first.  Otherwise, you risk creating a deadlock where two threads each hold one of the locks
26    // and is waiting for the other.
27    connection: Mutex<Option<Connection>>,
28    // It's important to use a separate mutex for the interrupt handle, since the whole point is to
29    // interrupt while another thread holds the connection mutex.  Since the only mutation is
30    // setting/unsetting the Arc, maybe this could be sped up by using something like
31    // `arc_swap::ArcSwap`, but that seems like overkill for our purposes. This mutex should rarely
32    // be contested and interrupt operations execute quickly.
33    interrupt_handle: Mutex<Option<Arc<SqlInterruptHandle>>>,
34}
35
36impl<CI: ConnectionInitializer> LazyDb<CI> {
37    /// Create a new LazyDb
38    ///
39    /// This does not open the connection and is non-blocking
40    pub fn new(path: &Path, open_flags: OpenFlags, connection_initializer: CI) -> Self {
41        Self {
42            path: path.to_owned(),
43            open_flags,
44            connection_initializer,
45            connection: Mutex::new(None),
46            interrupt_handle: Mutex::new(None),
47        }
48    }
49
50    /// Lock the database mutex and get a connection and interrupt scope.
51    ///
52    /// If the connection is closed, it will be opened.
53    ///
54    /// Calling `lock` again, or calling `close`, from the same thread while the mutex guard is
55    /// still alive will cause a deadlock.
56    pub fn lock(&self) -> Result<(MappedMutexGuard<'_, Connection>, SqlInterruptScope), Error> {
57        // Call get_conn first, then get_scope to ensure we acquire the locks in the correct order
58        let conn = self.get_conn()?;
59        let scope = self.get_scope(&conn)?;
60        Ok((conn, scope))
61    }
62
63    fn get_conn(&self) -> Result<MappedMutexGuard<'_, Connection>, Error> {
64        let mut guard = self.connection.lock();
65        // Open the database if it wasn't opened before.  Do this outside of the MutexGuard::map call to simplify the error handling
66        if guard.is_none() {
67            *guard = Some(open_database_with_flags(
68                &self.path,
69                self.open_flags,
70                &self.connection_initializer,
71            )?);
72        };
73        // Use MutexGuard::map to get a Connection rather than Option<Connection>.  The unwrap()
74        // call can't fail because of the previous code.
75        Ok(MutexGuard::map(guard, |conn_option| {
76            conn_option.as_mut().unwrap()
77        }))
78    }
79
80    fn get_scope(&self, conn: &Connection) -> Result<SqlInterruptScope, Error> {
81        let mut handle_guard = self.interrupt_handle.lock();
82        let result = match handle_guard.as_ref() {
83            Some(handle) => handle.begin_interrupt_scope(),
84            None => {
85                let handle = Arc::new(SqlInterruptHandle::new(conn));
86                register_interrupt(
87                    Arc::downgrade(&handle) as Weak<dyn AsRef<SqlInterruptHandle> + Send + Sync>
88                );
89                handle_guard.insert(handle).begin_interrupt_scope()
90            }
91        };
92        // If we see an Interrupted error when beginning the scope, it means that we're in shutdown
93        // mode.
94        result.map_err(|_| Error::Shutdown)
95    }
96
97    /// Close the database if it's open
98    ///
99    /// Pass interrupt=true to interrupt any in-progress queries before closing the database.
100    ///
101    /// Do not call `close` if you already have a lock on the database in the current thread, as
102    /// this will cause a deadlock.
103    pub fn close(&self, interrupt: bool) {
104        let mut interrupt_handle = self.interrupt_handle.lock();
105        if let Some(handle) = interrupt_handle.as_ref() {
106            if interrupt {
107                handle.interrupt();
108            }
109            *interrupt_handle = None;
110        }
111        // Drop the interrupt handle lock to avoid holding both locks at once.
112        drop(interrupt_handle);
113        *self.connection.lock() = None;
114    }
115
116    /// Interrupt any in-progress queries
117    pub fn interrupt(&self) {
118        if let Some(handle) = self.interrupt_handle.lock().as_ref() {
119            handle.interrupt();
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::open_database::test_utils::TestConnectionInitializer;
128
129    fn open_test_db() -> LazyDb<TestConnectionInitializer> {
130        LazyDb::new(
131            Path::new(":memory:"),
132            OpenFlags::default(),
133            TestConnectionInitializer::new(),
134        )
135    }
136
137    #[test]
138    fn test_interrupt() {
139        let lazy_db = open_test_db();
140        let (_, scope) = lazy_db.lock().unwrap();
141        assert!(!scope.was_interrupted());
142        lazy_db.interrupt();
143        assert!(scope.was_interrupted());
144    }
145
146    #[test]
147    fn interrupt_before_db_is_opened_should_not_fail() {
148        let lazy_db = open_test_db();
149        lazy_db.interrupt();
150    }
151}