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}