1use std::collections::BTreeMap;
8use std::ops::{Deref, DerefMut};
9use std::path::Path;
10use std::sync::{Mutex, OnceLock};
11
12use rusqlite::{Connection, OpenFlags};
13use serde::Serialize;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
18pub enum SqliteStore {
19 AftDb,
20 BlobStore,
21 CallgraphGeneration,
22 InspectScopeCache,
23 BreakerFile,
24}
25
26impl SqliteStore {
27 pub const ALL: [Self; 5] = [
28 Self::AftDb,
29 Self::BlobStore,
30 Self::CallgraphGeneration,
31 Self::InspectScopeCache,
32 Self::BreakerFile,
33 ];
34
35 pub const fn label(self) -> &'static str {
36 match self {
37 Self::AftDb => "aft.db",
38 Self::BlobStore => "blob_stores",
39 Self::CallgraphGeneration => "callgraph_generations",
40 Self::InspectScopeCache => "inspect_scope_caches",
41 Self::BreakerFile => "breaker_files",
42 }
43 }
44}
45
46#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
49pub struct SqliteStoreCount {
50 pub store: String,
51 pub count: u64,
52}
53
54#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
55pub struct SqliteConnectionSnapshot {
56 pub open_connections: u64,
57 pub open_by_store: Vec<SqliteStoreCount>,
58 pub uninstrumented_openers: Vec<String>,
62}
63
64pub const SQLITE_UNINSTRUMENTED_OPENERS: &[&str] = &[];
69
70fn live_counts() -> &'static Mutex<BTreeMap<SqliteStore, u64>> {
71 static COUNTS: OnceLock<Mutex<BTreeMap<SqliteStore, u64>>> = OnceLock::new();
72 COUNTS.get_or_init(|| Mutex::new(BTreeMap::new()))
73}
74
75fn register_open(store: SqliteStore) {
76 let mut counts = live_counts()
77 .lock()
78 .unwrap_or_else(std::sync::PoisonError::into_inner);
79 *counts.entry(store).or_default() += 1;
80 #[cfg(test)]
81 thread_counts::record(store, 1);
82}
83
84fn register_close(store: SqliteStore) {
85 let mut counts = live_counts()
86 .lock()
87 .unwrap_or_else(std::sync::PoisonError::into_inner);
88 let count = counts.entry(store).or_default();
89 *count = count.saturating_sub(1);
90 #[cfg(test)]
91 thread_counts::record(store, -1);
92}
93
94#[cfg(test)]
99pub(crate) mod thread_counts {
100 use super::SqliteStore;
101 use std::cell::RefCell;
102 use std::collections::BTreeMap;
103
104 thread_local! {
105 static COUNTS: RefCell<BTreeMap<SqliteStore, i64>> = RefCell::new(BTreeMap::new());
106 static OPENS: RefCell<BTreeMap<SqliteStore, u64>> = RefCell::new(BTreeMap::new());
107 }
108
109 pub(super) fn record(store: SqliteStore, delta: i64) {
110 COUNTS.with(|counts| *counts.borrow_mut().entry(store).or_default() += delta);
111 if delta > 0 {
112 OPENS.with(|counts| *counts.borrow_mut().entry(store).or_default() += 1);
113 }
114 }
115
116 pub(crate) fn total_opens_on_this_thread(store: SqliteStore) -> u64 {
117 OPENS.with(|counts| counts.borrow().get(&store).copied().unwrap_or(0))
118 }
119
120 pub(crate) fn open_on_this_thread(store: SqliteStore) -> i64 {
121 COUNTS.with(|counts| counts.borrow().get(&store).copied().unwrap_or(0))
122 }
123}
124
125pub fn connection_snapshot() -> SqliteConnectionSnapshot {
128 let counts = live_counts()
129 .lock()
130 .unwrap_or_else(std::sync::PoisonError::into_inner);
131 let open_by_store = SqliteStore::ALL
132 .into_iter()
133 .map(|store| SqliteStoreCount {
134 store: store.label().to_string(),
135 count: counts.get(&store).copied().unwrap_or(0),
136 })
137 .collect::<Vec<_>>();
138 let open_connections = open_by_store.iter().map(|row| row.count).sum();
139 SqliteConnectionSnapshot {
140 open_connections,
141 open_by_store,
142 uninstrumented_openers: SQLITE_UNINSTRUMENTED_OPENERS
143 .iter()
144 .map(|opener| (*opener).to_string())
145 .collect(),
146 }
147}
148
149#[derive(Debug)]
153pub struct TrackedConnection {
154 connection: Option<Connection>,
155 store: SqliteStore,
156}
157
158impl TrackedConnection {
159 pub fn open(path: &Path, store: SqliteStore) -> rusqlite::Result<Self> {
160 Self::from_connection(Connection::open(path)?, store)
161 }
162
163 pub fn open_with_flags(
164 path: &str,
165 flags: OpenFlags,
166 store: SqliteStore,
167 ) -> rusqlite::Result<Self> {
168 Self::from_connection(Connection::open_with_flags(path, flags)?, store)
169 }
170
171 pub fn open_path_with_flags(
172 path: &Path,
173 flags: OpenFlags,
174 store: SqliteStore,
175 ) -> rusqlite::Result<Self> {
176 Self::from_connection(Connection::open_with_flags(path, flags)?, store)
177 }
178
179 pub fn open_in_memory(store: SqliteStore) -> rusqlite::Result<Self> {
180 Self::from_connection(Connection::open_in_memory()?, store)
181 }
182
183 pub fn from_connection(connection: Connection, store: SqliteStore) -> rusqlite::Result<Self> {
184 register_open(store);
185 Ok(Self {
186 connection: Some(connection),
187 store,
188 })
189 }
190}
191
192impl Deref for TrackedConnection {
193 type Target = Connection;
194
195 fn deref(&self) -> &Self::Target {
196 self.connection
197 .as_ref()
198 .expect("tracked SQLite connection accessed after drop")
199 }
200}
201
202impl DerefMut for TrackedConnection {
203 fn deref_mut(&mut self) -> &mut Self::Target {
204 self.connection
205 .as_mut()
206 .expect("tracked SQLite connection accessed after drop")
207 }
208}
209
210impl Drop for TrackedConnection {
211 fn drop(&mut self) {
212 drop(self.connection.take());
215 register_close(self.store);
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn store_count(snapshot: &SqliteConnectionSnapshot, store: SqliteStore) -> u64 {
224 snapshot
225 .open_by_store
226 .iter()
227 .find(|row| row.store == store.label())
228 .expect("every store has a snapshot row")
229 .count
230 }
231
232 #[test]
233 fn tracked_connection_counts_open_and_close_at_each_seam() {
234 use super::thread_counts::open_on_this_thread;
235 let baseline_aft = open_on_this_thread(SqliteStore::AftDb);
236 let baseline_callgraph = open_on_this_thread(SqliteStore::CallgraphGeneration);
237 let dir = tempfile::tempdir().expect("tempdir");
238 let aft = TrackedConnection::open(&dir.path().join("aft.db"), SqliteStore::AftDb)
239 .expect("open aft db");
240 let callgraph = TrackedConnection::open(
241 &dir.path().join("graph.sqlite"),
242 SqliteStore::CallgraphGeneration,
243 )
244 .expect("open callgraph");
245 assert_eq!(open_on_this_thread(SqliteStore::AftDb), baseline_aft + 1);
246 assert_eq!(
247 open_on_this_thread(SqliteStore::CallgraphGeneration),
248 baseline_callgraph + 1
249 );
250 let snapshot = connection_snapshot();
254 assert!(store_count(&snapshot, SqliteStore::AftDb) >= 1);
255 assert!(store_count(&snapshot, SqliteStore::CallgraphGeneration) >= 1);
256 assert!(snapshot.open_connections >= 2);
257
258 drop(aft);
259 assert_eq!(open_on_this_thread(SqliteStore::AftDb), baseline_aft);
260 assert_eq!(
261 open_on_this_thread(SqliteStore::CallgraphGeneration),
262 baseline_callgraph + 1
263 );
264
265 drop(callgraph);
266 assert_eq!(open_on_this_thread(SqliteStore::AftDb), baseline_aft);
267 assert_eq!(
268 open_on_this_thread(SqliteStore::CallgraphGeneration),
269 baseline_callgraph
270 );
271 }
272
273 #[test]
274 fn every_uninstrumented_production_opener_is_documented() {
275 assert!(SQLITE_UNINSTRUMENTED_OPENERS.is_empty());
279 }
280}