whatsapp_rust_sqlite_storage/
shared.rs1use diesel::sqlite::SqliteConnection;
9use std::sync::Arc;
10use wacore::store::error::{Result, StoreError};
11
12use crate::sqlite_store::{SqlitePool, SqliteStore};
13
14#[derive(Clone)]
18pub struct SharedSqlite {
19 pool: SqlitePool,
20 semaphore: Arc<tokio::sync::Semaphore>,
21 reads: Option<crate::sqlite_store::ReadPool>,
22}
23
24impl SharedSqlite {
25 pub async fn run<F, T>(&self, f: F) -> Result<T>
34 where
35 F: FnOnce(&mut SqliteConnection) -> Result<T> + Send + 'static,
36 T: Send + 'static,
37 {
38 Self::run_on(self.pool.clone(), Arc::clone(&self.semaphore), f).await
39 }
40
41 pub async fn read<F, T>(&self, f: F) -> Result<T>
66 where
67 F: FnOnce(&mut SqliteConnection) -> Result<T> + Send + 'static,
68 T: Send + 'static,
69 {
70 let (pool, semaphore) = match &self.reads {
71 Some(reads) => (reads.pool.clone(), Arc::clone(&reads.semaphore)),
72 None => (self.pool.clone(), Arc::clone(&self.semaphore)),
73 };
74 Self::run_on(pool, semaphore, move |conn| read_snapshot(conn, f)).await
75 }
76
77 async fn run_on<F, T>(
78 pool: SqlitePool,
79 semaphore: Arc<tokio::sync::Semaphore>,
80 f: F,
81 ) -> Result<T>
82 where
83 F: FnOnce(&mut SqliteConnection) -> Result<T> + Send + 'static,
84 T: Send + 'static,
85 {
86 let permit = semaphore
87 .acquire_owned()
88 .await
89 .map_err(|e| StoreError::Database(Box::new(e)))?;
90 tokio::task::spawn_blocking(move || {
91 let _permit = permit;
92 let mut conn = pool
93 .get()
94 .map_err(|e| StoreError::Connection(Box::new(e)))?;
95 f(&mut conn)
96 })
97 .await
98 .map_err(|e| StoreError::Database(Box::new(e)))?
99 }
100}
101
102fn read_snapshot<T>(
109 conn: &mut SqliteConnection,
110 f: impl FnOnce(&mut SqliteConnection) -> Result<T>,
111) -> Result<T> {
112 use diesel::connection::Connection as _;
113
114 enum TxnError {
115 Store(StoreError),
116 Diesel(diesel::result::Error),
117 }
118 impl From<diesel::result::Error> for TxnError {
119 fn from(e: diesel::result::Error) -> Self {
120 Self::Diesel(e)
121 }
122 }
123 conn.transaction::<T, TxnError, _>(|conn| f(conn).map_err(TxnError::Store))
124 .map_err(|e| match e {
125 TxnError::Store(e) => e,
126 TxnError::Diesel(e) => StoreError::Database(Box::new(e)),
127 })
128}
129
130impl SqliteStore {
131 pub fn shared(&self) -> SharedSqlite {
134 SharedSqlite {
135 pool: self.pool.clone(),
136 semaphore: self.db_semaphore.clone(),
137 reads: self.reads.clone(),
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use diesel::prelude::*;
145
146 use crate::sqlite_store::SqliteStore;
147 use wacore::store::error::StoreError;
148
149 fn unique_db_name(tag: &str) -> String {
150 use portable_atomic::AtomicU64;
151 use std::sync::atomic::Ordering;
152 static COUNTER: AtomicU64 = AtomicU64::new(0);
153 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
154 format!(
155 "file:memdb_shared_{tag}_{}_{}?mode=memory&cache=shared",
156 std::process::id(),
157 id
158 )
159 }
160
161 async fn create_test_store(tag: &str) -> SqliteStore {
162 SqliteStore::new(&unique_db_name(tag))
163 .await
164 .expect("Failed to create test store")
165 }
166
167 fn db_err(e: diesel::result::Error) -> StoreError {
168 StoreError::Database(Box::new(e))
169 }
170
171 #[tokio::test]
172 async fn shared_handle_sees_the_same_database() {
173 let store = create_test_store("same_db").await;
174 let shared = store.shared();
175
176 shared
177 .run(|conn| {
178 diesel::sql_query("CREATE TABLE sibling_data (k TEXT PRIMARY KEY, v TEXT)")
179 .execute(conn)
180 .map_err(db_err)?;
181 diesel::sql_query("INSERT INTO sibling_data (k, v) VALUES ('a', 'b')")
182 .execute(conn)
183 .map_err(db_err)?;
184 Ok(())
185 })
186 .await
187 .expect("create + insert through shared handle");
188
189 #[derive(QueryableByName)]
191 struct Row {
192 #[diesel(sql_type = diesel::sql_types::Text)]
193 v: String,
194 }
195 let rows: Vec<Row> = shared
196 .clone()
197 .run(|conn| {
198 diesel::sql_query("SELECT v FROM sibling_data WHERE k = 'a'")
199 .load(conn)
200 .map_err(db_err)
201 })
202 .await
203 .expect("read through cloned handle");
204 assert_eq!(rows.len(), 1);
205 assert_eq!(rows[0].v, "b");
206 }
207
208 struct TempDb(std::path::PathBuf);
211
212 impl TempDb {
213 fn new(tag: &str) -> Self {
214 use portable_atomic::AtomicU64;
215 use std::sync::atomic::Ordering;
216 static COUNTER: AtomicU64 = AtomicU64::new(0);
217 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
218 let mut path = std::env::temp_dir();
219 path.push(format!("wa_shared_{tag}_{}_{id}.db", std::process::id()));
220 let _ = std::fs::remove_file(&path);
221 Self(path)
222 }
223
224 fn url(&self) -> String {
225 self.0.to_string_lossy().into_owned()
226 }
227 }
228
229 impl Drop for TempDb {
230 fn drop(&mut self) {
231 for suffix in ["", "-wal", "-shm"] {
232 let mut p = self.0.clone().into_os_string();
233 p.push(suffix);
234 let _ = std::fs::remove_file(p);
235 }
236 }
237 }
238
239 fn park_until_released(rx: &std::sync::mpsc::Receiver<()>) {
243 let _ = rx.recv_timeout(std::time::Duration::from_secs(20));
244 }
245
246 #[tokio::test]
250 async fn reads_run_concurrently_when_reader_connections_are_configured() {
251 use crate::sqlite_store::SqliteStoreConfig;
252 use std::sync::Arc;
253
254 let db = TempDb::new("read_concurrency");
255 let store = SqliteStore::with_config(
256 &db.url(),
257 SqliteStoreConfig {
258 read_pool_size: 4,
259 ..Default::default()
260 },
261 )
262 .await
263 .expect("store with reader connections");
264 assert!(store.reads.is_some(), "a file-backed store reaches WAL");
265 let shared = store.shared();
266
267 let (ready_tx, mut ready_rx) = tokio::sync::mpsc::unbounded_channel();
271 let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
272 let release_rx = Arc::new(std::sync::Mutex::new(release_rx));
273 let mut readers = Vec::new();
274 for _ in 0..4 {
275 let shared = shared.clone();
276 let ready_tx = ready_tx.clone();
277 let release_rx = Arc::clone(&release_rx);
278 readers.push(tokio::spawn(async move {
279 shared
280 .read(move |conn| {
281 diesel::sql_query("SELECT 1")
282 .execute(conn)
283 .map_err(db_err)?;
284 let _ = ready_tx.send(());
285 park_until_released(&release_rx.lock().expect("release rx"));
288 Ok(())
289 })
290 .await
291 }));
292 }
293 drop(ready_tx);
294
295 for _ in 0..4 {
297 tokio::time::timeout(std::time::Duration::from_secs(10), ready_rx.recv())
298 .await
299 .expect("readers must overlap, not serialize")
300 .expect("reader alive");
301 }
302 for _ in 0..4 {
303 let _ = release_tx.send(());
304 }
305 for reader in readers {
306 reader.await.expect("join").expect("read");
307 }
308 }
309
310 #[tokio::test]
313 async fn a_write_proceeds_while_every_reader_permit_is_held() {
314 use crate::sqlite_store::SqliteStoreConfig;
315 use std::sync::Arc;
316
317 let db = TempDb::new("write_not_starved");
318 let store = SqliteStore::with_config(
319 &db.url(),
320 SqliteStoreConfig {
321 read_pool_size: 2,
322 ..Default::default()
323 },
324 )
325 .await
326 .expect("store with reader connections");
327 let shared = store.shared();
328 shared
329 .run(|conn| {
330 diesel::sql_query("CREATE TABLE probe (k INTEGER PRIMARY KEY)")
331 .execute(conn)
332 .map_err(db_err)?;
333 Ok(())
334 })
335 .await
336 .expect("create");
337
338 let (ready_tx, mut ready_rx) = tokio::sync::mpsc::unbounded_channel();
339 let (release_tx, release_rx) = std::sync::mpsc::channel::<()>();
340 let release_rx = Arc::new(std::sync::Mutex::new(release_rx));
341 let mut readers = Vec::new();
342 for _ in 0..2 {
343 let shared = shared.clone();
344 let ready_tx = ready_tx.clone();
345 let release_rx = Arc::clone(&release_rx);
346 readers.push(tokio::spawn(async move {
347 shared
348 .read(move |conn| {
349 diesel::sql_query("SELECT 1")
350 .execute(conn)
351 .map_err(db_err)?;
352 let _ = ready_tx.send(());
353 park_until_released(&release_rx.lock().expect("release rx"));
354 Ok(())
355 })
356 .await
357 }));
358 }
359 drop(ready_tx);
360
361 for _ in 0..2 {
364 tokio::time::timeout(std::time::Duration::from_secs(10), ready_rx.recv())
365 .await
366 .expect("readers must check out")
367 .expect("reader alive");
368 }
369
370 let wrote = tokio::time::timeout(
371 std::time::Duration::from_secs(10),
372 shared.run(|conn| {
373 diesel::sql_query("INSERT INTO probe (k) VALUES (1)")
374 .execute(conn)
375 .map_err(db_err)?;
376 Ok(())
377 }),
378 )
379 .await;
380 for _ in 0..2 {
383 let _ = release_tx.send(());
384 }
385 wrote
386 .expect("the writer must not queue behind readers")
387 .expect("insert");
388 for reader in readers {
389 reader.await.expect("join").expect("read");
390 }
391 }
392
393 #[tokio::test]
396 async fn a_write_through_the_read_path_is_refused() {
397 use crate::sqlite_store::SqliteStoreConfig;
398
399 let db = TempDb::new("read_only");
400 let store = SqliteStore::with_config(
401 &db.url(),
402 SqliteStoreConfig {
403 read_pool_size: 1,
404 ..Default::default()
405 },
406 )
407 .await
408 .expect("store with reader connections");
409 let shared = store.shared();
410
411 let result = shared
412 .read(|conn| {
413 diesel::sql_query("CREATE TABLE nope (k INTEGER)")
414 .execute(conn)
415 .map_err(db_err)?;
416 Ok(())
417 })
418 .await;
419 assert!(matches!(result, Err(StoreError::Database(_))));
420 }
421
422 #[tokio::test]
426 async fn reader_connections_are_declined_without_wal() {
427 use crate::sqlite_store::SqliteStoreConfig;
428
429 let store = SqliteStore::with_config(
430 &unique_db_name("no_wal"),
431 SqliteStoreConfig {
432 read_pool_size: 4,
433 ..Default::default()
434 },
435 )
436 .await
437 .expect("in-memory store still opens");
438 assert!(store.reads.is_none(), "no WAL, no reader pool");
439
440 store
442 .shared()
443 .read(|conn| {
444 diesel::sql_query("SELECT 1")
445 .execute(conn)
446 .map_err(db_err)?;
447 Ok(())
448 })
449 .await
450 .expect("reads fall back to the write permit");
451 }
452
453 #[tokio::test]
456 async fn reader_connections_are_declined_for_shared_cache() {
457 use crate::sqlite_store::SqliteStoreConfig;
458
459 let db = TempDb::new("shared_cache");
460 let store = SqliteStore::with_config(
461 &format!("file:{}?cache=shared", db.url()),
462 SqliteStoreConfig {
463 read_pool_size: 4,
464 ..Default::default()
465 },
466 )
467 .await
468 .expect("shared-cache store still opens");
469 assert!(
470 store.reads.is_none(),
471 "shared cache serializes readers against the writer anyway"
472 );
473
474 let store = SqliteStore::with_config(
477 &db.url(),
478 SqliteStoreConfig {
479 read_pool_size: 4,
480 ..Default::default()
481 },
482 )
483 .await
484 .expect("private-cache store opens");
485 assert!(
486 store.reads.is_some(),
487 "private cache under WAL gets readers"
488 );
489 }
490
491 #[tokio::test]
495 async fn resource_report_counts_reader_connections() {
496 use crate::sqlite_store::SqliteStoreConfig;
497 use wacore::store::traits::DeviceStore;
498
499 let db = TempDb::new("report_readers");
500 let config = || SqliteStoreConfig {
501 read_pool_size: 3,
502 ..Default::default()
503 };
504
505 let with_readers = SqliteStore::with_config(&db.url(), config())
506 .await
507 .expect("store opens");
508 assert!(with_readers.reads.is_some(), "readers are configured");
509 with_readers
512 .shared()
513 .read(|conn| {
514 diesel::sql_query("SELECT 1")
515 .execute(conn)
516 .map_err(db_err)?;
517 Ok(())
518 })
519 .await
520 .expect("read succeeds");
521
522 let baseline = TempDb::new("report_no_readers");
523 let without = SqliteStore::new(&baseline.url())
524 .await
525 .expect("store opens");
526
527 let (a, b) = (
528 with_readers.resource_report().await,
529 without.resource_report().await,
530 );
531 let (Some(with_mem), Some(without_mem)) = (a.memory_bytes, b.memory_bytes) else {
532 panic!("both stores report a cache estimate");
533 };
534 assert!(
535 with_mem > without_mem,
536 "reader caches must widen the bound: {with_mem} vs {without_mem}"
537 );
538 }
539
540 #[tokio::test]
543 async fn read_falls_back_to_the_write_permit_by_default() {
544 let store = create_test_store("read_default").await;
545 let shared = store.shared();
546 shared
547 .read(|conn| {
548 diesel::sql_query("SELECT 1")
549 .execute(conn)
550 .map_err(db_err)?;
551 Ok(())
552 })
553 .await
554 .expect("reads work with no reader connections configured");
555 }
556
557 #[tokio::test]
558 async fn shared_handle_propagates_closure_errors() {
559 let store = create_test_store("err").await;
560 let result = store
561 .shared()
562 .run(|conn| {
563 diesel::sql_query("SELECT * FROM does_not_exist")
564 .execute(conn)
565 .map_err(db_err)
566 })
567 .await;
568 assert!(matches!(result, Err(StoreError::Database(_))));
569 }
570}