1use std::panic::AssertUnwindSafe;
38use std::path::{Path, PathBuf};
39use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
40use std::sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, mpsc};
41use std::thread;
42use std::time::{Duration, Instant};
43
44use notify::{RecursiveMode, Watcher as _};
45
46pub const SQLITE_WATCH_SWEEP: Duration = Duration::from_millis(5_000);
48const SQLITE_WATCH_COALESCE: Duration = Duration::from_millis(10);
51const SQLITE_WATCH_RECOVERY_BACKOFF_MIN: Duration = Duration::from_millis(100);
52const SQLITE_WATCH_RECOVERY_BACKOFF_MAX: Duration = Duration::from_millis(30_000);
53const MAX_REGISTRATIONS_IN_FLIGHT: usize = 64;
65static REGISTRATIONS_IN_FLIGHT: AtomicUsize = AtomicUsize::new(0);
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SqliteWatchTick {
72 Changed,
74 Sweep,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum SqliteWatchControl {
81 Handled,
83 Failed,
87 Stop,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[must_use]
94pub enum SqliteWatchJoin {
95 Joined,
98 JoinedAfterPanic,
101 UnjoinedOnWorkerThread,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
113#[non_exhaustive]
114pub enum SqliteWatchRegistration {
115 Pending,
118 Registered,
120 Failed(String),
123 Cancelled,
126}
127
128#[derive(Debug, Clone)]
131pub struct SqliteWatchStopHandle {
132 control: Arc<WatchControl>,
133}
134
135impl SqliteWatchStopHandle {
136 pub fn request_stop(&self) {
139 self.control.request_stop();
140 }
141}
142
143#[derive(Debug)]
144struct WatchControl {
145 stop_requested: Arc<AtomicBool>,
146 wake: mpsc::Sender<()>,
147}
148
149impl WatchControl {
150 fn request_stop(&self) {
151 self.stop_requested.store(true, Ordering::Release);
152 let _ = self.wake.send(());
155 }
156}
157
158enum WorkerCustody {
160 Running(thread::JoinHandle<()>),
161 Joined(SqliteWatchJoin),
162}
163
164enum RegistrationSlot {
166 Pending,
167 Registered(Option<Box<dyn WatchSource>>),
169 Failed(String),
170 Cancelled,
171}
172
173struct InFlightPlace;
176
177impl InFlightPlace {
178 fn take() -> Result<Self, usize> {
180 REGISTRATIONS_IN_FLIGHT
181 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |running| {
182 (running < MAX_REGISTRATIONS_IN_FLIGHT).then_some(running + 1)
183 })
184 .map(|_| Self)
185 }
186}
187
188impl Drop for InFlightPlace {
189 fn drop(&mut self) {
190 REGISTRATIONS_IN_FLIGHT.fetch_sub(1, Ordering::AcqRel);
191 }
192}
193
194struct RegistrationThread {
196 handle: thread::JoinHandle<()>,
197 directory: PathBuf,
198 name: String,
199 started: Instant,
200}
201
202impl RegistrationThread {
203 fn describe(&self) -> SqliteWatchRetainedRegistration {
204 SqliteWatchRetainedRegistration {
205 directory: self.directory.clone(),
206 thread_name: self.name.clone(),
207 running_for: self.started.elapsed(),
208 }
209 }
210}
211
212struct RegistrationCustody {
215 slot: Arc<Mutex<RegistrationSlot>>,
216 thread: Option<RegistrationThread>,
218}
219
220impl RegistrationCustody {
221 fn spawn(
227 watch_name: &str,
228 backend: Box<dyn WatchBackend>,
229 targets: WatchTargets,
230 wake: mpsc::Sender<()>,
231 stop_requested: Arc<AtomicBool>,
232 ) -> Self {
233 let slot = Arc::new(Mutex::new(RegistrationSlot::Pending));
234 let place = match InFlightPlace::take() {
235 Ok(place) => place,
236 Err(running) => {
237 settle_failed(
238 &slot,
239 format!(
240 "{running} sqlite change watch notification registrations are already \
241 pending (at most {MAX_REGISTRATIONS_IN_FLIGHT})"
242 ),
243 );
244 return Self { slot, thread: None };
245 }
246 };
247 let thread_slot = Arc::clone(&slot);
248 let directory = targets.parent.clone();
249 let name = format!("{watch_name}-register");
250 let started = Instant::now();
251 let spawned = thread::Builder::new().name(name.clone()).spawn(move || {
252 let _place = place;
254 register(
255 backend.as_ref(),
256 &targets,
257 wake,
258 &stop_requested,
259 &thread_slot,
260 );
261 });
262 match spawned {
263 Ok(handle) => Self {
264 slot,
265 thread: Some(RegistrationThread {
266 handle,
267 directory,
268 name,
269 started,
270 }),
271 },
272 Err(error) => {
273 settle_failed(
274 &slot,
275 format!("cannot start the sqlite change watch registration thread: {error}"),
276 );
277 Self { slot, thread: None }
278 }
279 }
280 }
281
282 fn state(&self) -> SqliteWatchRegistration {
283 match &*lock(&self.slot) {
284 RegistrationSlot::Pending => SqliteWatchRegistration::Pending,
285 RegistrationSlot::Registered(_) => SqliteWatchRegistration::Registered,
286 RegistrationSlot::Failed(reason) => SqliteWatchRegistration::Failed(reason.clone()),
287 RegistrationSlot::Cancelled => SqliteWatchRegistration::Cancelled,
288 }
289 }
290
291 fn pending(&self) -> Option<SqliteWatchRetainedRegistration> {
295 if !matches!(*lock(&self.slot), RegistrationSlot::Pending) {
296 return None;
297 }
298 self.thread.as_ref().map(RegistrationThread::describe)
299 }
300
301 fn stop(&mut self) {
305 let source = match &mut *lock(&self.slot) {
306 RegistrationSlot::Registered(source) => source.take(),
307 _ => None,
308 };
309 drop(source);
310 if self
311 .thread
312 .as_ref()
313 .is_some_and(|thread| thread.handle.is_finished())
314 && let Some(thread) = self.thread.take()
315 {
316 let _ = thread.handle.join();
317 }
318 }
319
320 fn release(&mut self) {
323 self.stop();
324 if let Some(thread) = self.thread.take() {
325 retain_registration(thread);
326 }
327 }
328}
329
330pub struct SqliteChangeWatch {
337 control: Arc<WatchControl>,
338 registration: RegistrationCustody,
339 worker: WorkerCustody,
340}
341
342impl std::fmt::Debug for SqliteChangeWatch {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 f.debug_struct("SqliteChangeWatch").finish_non_exhaustive()
345 }
346}
347
348impl SqliteChangeWatch {
349 pub fn stop_handle(&self) -> SqliteWatchStopHandle {
352 SqliteWatchStopHandle {
353 control: Arc::clone(&self.control),
354 }
355 }
356
357 pub fn request_stop(&self) {
359 self.control.request_stop();
360 }
361
362 pub fn on_worker_thread(&self) -> bool {
365 self.worker_thread_id() == Some(thread::current().id())
366 }
367
368 pub fn worker_thread_id(&self) -> Option<thread::ThreadId> {
370 match &self.worker {
371 WorkerCustody::Running(worker) => Some(worker.thread().id()),
372 WorkerCustody::Joined(_) => None,
373 }
374 }
375
376 pub fn is_finished(&self) -> bool {
379 match &self.worker {
380 WorkerCustody::Running(worker) => worker.is_finished(),
381 WorkerCustody::Joined(_) => true,
382 }
383 }
384
385 pub fn registration(&self) -> SqliteWatchRegistration {
388 self.registration.state()
389 }
390
391 pub fn pending_registration(&self) -> Option<SqliteWatchRetainedRegistration> {
402 self.registration.pending()
403 }
404
405 pub fn stop_and_join(&mut self) -> SqliteWatchJoin {
420 self.control.request_stop();
421 self.registration.stop();
422 if self.on_worker_thread() {
423 return SqliteWatchJoin::UnjoinedOnWorkerThread;
424 }
425 let outcome = match std::mem::replace(
426 &mut self.worker,
427 WorkerCustody::Joined(SqliteWatchJoin::Joined),
428 ) {
429 WorkerCustody::Running(worker) => match worker.join() {
430 Ok(()) => SqliteWatchJoin::Joined,
431 Err(_) => SqliteWatchJoin::JoinedAfterPanic,
432 },
433 WorkerCustody::Joined(outcome) => outcome,
434 };
435 self.worker = WorkerCustody::Joined(outcome);
436 outcome
437 }
438}
439
440impl Drop for SqliteChangeWatch {
441 fn drop(&mut self) {
442 self.control.request_stop();
446 self.registration.release();
447 }
448}
449
450static RETAINED_REGISTRATIONS: OnceLock<Mutex<Vec<RegistrationThread>>> = OnceLock::new();
452
453fn retained() -> &'static Mutex<Vec<RegistrationThread>> {
454 RETAINED_REGISTRATIONS.get_or_init(Mutex::default)
455}
456
457#[derive(Debug, Clone, PartialEq, Eq)]
461#[non_exhaustive]
462pub struct SqliteWatchRetainedRegistration {
463 pub directory: PathBuf,
465 pub thread_name: String,
467 pub running_for: Duration,
469}
470
471pub fn retained_registrations() -> Vec<SqliteWatchRetainedRegistration> {
476 let mut retained = lock(retained());
477 reap(&mut retained);
478 retained.iter().map(RegistrationThread::describe).collect()
479}
480
481fn retain_registration(thread: RegistrationThread) {
482 let mut retained = lock(retained());
483 reap(&mut retained);
484 retained.push(thread);
485}
486
487fn reap(retained: &mut Vec<RegistrationThread>) {
489 let (returned, running): (Vec<_>, Vec<_>) = std::mem::take(retained)
490 .into_iter()
491 .partition(|thread| thread.handle.is_finished());
492 *retained = running;
493 for thread in returned {
494 let _ = thread.handle.join();
495 }
496}
497
498fn sidecar_path(path: &Path, suffix: &str) -> PathBuf {
499 let mut value = path.as_os_str().to_os_string();
500 value.push(suffix);
501 PathBuf::from(value)
502}
503
504pub fn sqlite_watch_paths(path: &Path) -> Vec<PathBuf> {
506 vec![
507 path.to_path_buf(),
508 sidecar_path(path, "-wal"),
509 sidecar_path(path, "-shm"),
510 ]
511}
512
513fn sqlite_watch_targets(db_path: &Path) -> Result<(PathBuf, Vec<PathBuf>), String> {
514 let file_name = db_path
515 .file_name()
516 .ok_or_else(|| format!("{} has no database filename", db_path.display()))?;
517 let parent = db_path
518 .parent()
519 .filter(|parent| !parent.as_os_str().is_empty())
520 .unwrap_or_else(|| Path::new("."));
521 let parent = parent.canonicalize().map_err(|error| {
522 format!(
523 "cannot resolve {} for sqlite watch: {error}",
524 parent.display()
525 )
526 })?;
527 let watched_paths = sqlite_watch_paths(&parent.join(file_name));
531 Ok((parent, watched_paths))
532}
533
534pub fn sqlite_watch_event_relevant(
536 event: ¬ify::Event,
537 parent: &Path,
538 watched_paths: &[PathBuf],
539) -> bool {
540 if matches!(event.kind, notify::EventKind::Access(_)) {
541 return false;
542 }
543 if event.paths.is_empty() {
544 return true;
545 }
546 event.paths.iter().any(|path| {
547 path == parent
548 || watched_paths.iter().any(|watched| {
549 path == watched
550 || (path.parent() == watched.parent()
551 && path.file_name() == watched.file_name())
552 })
553 })
554}
555
556struct WatchTargets {
559 parent: PathBuf,
560 watched_paths: Vec<PathBuf>,
561}
562
563trait WatchBackend: Send {
566 fn create(
569 &self,
570 targets: &WatchTargets,
571 wake: mpsc::Sender<()>,
572 ) -> Result<Box<dyn WatchSource>, String>;
573}
574
575trait WatchSource: Send {
577 fn watch(&mut self, directory: &Path) -> Result<(), String>;
581}
582
583struct NotifyBackend;
585
586impl WatchBackend for NotifyBackend {
587 fn create(
588 &self,
589 targets: &WatchTargets,
590 wake: mpsc::Sender<()>,
591 ) -> Result<Box<dyn WatchSource>, String> {
592 let parent = targets.parent.clone();
593 let watched_paths = targets.watched_paths.clone();
594 let watcher = notify::recommended_watcher(move |result: notify::Result<notify::Event>| {
595 match result {
596 Ok(event) if sqlite_watch_event_relevant(&event, &parent, &watched_paths) => {
597 let _ = wake.send(());
598 }
599 Ok(_) => {}
600 Err(error) => {
601 tracing::warn!(error = %error, "sqlite change watch reported an error");
603 }
604 }
605 })
606 .map_err(|error| format!("cannot create the sqlite change watcher: {error}"))?;
607 Ok(Box::new(NotifySource(watcher)))
608 }
609}
610
611struct NotifySource(notify::RecommendedWatcher);
612
613impl WatchSource for NotifySource {
614 fn watch(&mut self, directory: &Path) -> Result<(), String> {
615 self.0
616 .watch(directory, RecursiveMode::NonRecursive)
617 .map_err(|error| format!("cannot watch {}: {error}", directory.display()))
618 }
619}
620
621#[cfg_attr(not(any(test, feature = "test-support")), allow(unused_variables))]
624fn watch_backend(parent: &Path) -> Box<dyn WatchBackend> {
625 #[cfg(any(test, feature = "test-support"))]
626 if let Some(held) = test_support::held_backend(parent) {
627 return held;
628 }
629 Box::new(NotifyBackend)
630}
631
632fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
633 mutex.lock().unwrap_or_else(PoisonError::into_inner)
634}
635
636pub fn start_sqlite_change_watch(
651 db_path: &Path,
652 thread_name: &str,
653 sweep: Duration,
654 on_tick: impl FnMut(SqliteWatchTick) -> SqliteWatchControl + Send + 'static,
655) -> Result<SqliteChangeWatch, String> {
656 start_with_backend(db_path, thread_name, sweep, on_tick, watch_backend)
657}
658
659fn start_with_backend(
660 db_path: &Path,
661 thread_name: &str,
662 sweep: Duration,
663 mut on_tick: impl FnMut(SqliteWatchTick) -> SqliteWatchControl + Send + 'static,
664 backend: impl FnOnce(&Path) -> Box<dyn WatchBackend>,
665) -> Result<SqliteChangeWatch, String> {
666 let (parent, watched_paths) = sqlite_watch_targets(db_path)?;
667 let backend = backend(&parent);
668 let targets = WatchTargets {
669 parent,
670 watched_paths,
671 };
672 let (wake_tx, wake_rx) = mpsc::channel::<()>();
673 let stop_requested = Arc::new(AtomicBool::new(false));
674 let worker_stop_requested = Arc::clone(&stop_requested);
675
676 let worker = thread::Builder::new()
679 .name(thread_name.to_string())
680 .spawn(move || {
681 let mut recovery_backoff = SQLITE_WATCH_RECOVERY_BACKOFF_MIN;
682 let mut recovery_deadline: Option<Instant> = None;
683 loop {
684 let wait = recovery_deadline
685 .map(|deadline| deadline.saturating_duration_since(Instant::now()))
686 .unwrap_or(sweep);
687 let tick = match wake_rx.recv_timeout(wait) {
688 Ok(()) => SqliteWatchTick::Changed,
689 Err(mpsc::RecvTimeoutError::Timeout) => SqliteWatchTick::Sweep,
690 Err(mpsc::RecvTimeoutError::Disconnected) => break,
692 };
693 if worker_stop_requested.load(Ordering::Acquire) {
694 break;
695 }
696 if tick == SqliteWatchTick::Changed {
697 thread::sleep(SQLITE_WATCH_COALESCE);
698 }
699 while wake_rx.try_recv().is_ok() {}
700 if worker_stop_requested.load(Ordering::Acquire) {
703 break;
704 }
705 if recovery_deadline.is_some_and(|deadline| Instant::now() < deadline) {
706 continue;
709 }
710 match on_tick(tick) {
711 SqliteWatchControl::Handled => {
712 recovery_deadline = None;
713 recovery_backoff = SQLITE_WATCH_RECOVERY_BACKOFF_MIN;
714 }
715 SqliteWatchControl::Failed => {
716 recovery_deadline = Some(Instant::now() + recovery_backoff);
717 recovery_backoff = recovery_backoff
718 .saturating_mul(2)
719 .min(SQLITE_WATCH_RECOVERY_BACKOFF_MAX);
720 }
721 SqliteWatchControl::Stop => break,
722 }
723 }
724 })
725 .map_err(|error| format!("cannot start the sqlite change watch thread: {error}"))?;
726
727 let registration = RegistrationCustody::spawn(
728 thread_name,
729 backend,
730 targets,
731 wake_tx.clone(),
732 Arc::clone(&stop_requested),
733 );
734 Ok(SqliteChangeWatch {
735 control: Arc::new(WatchControl {
736 stop_requested,
737 wake: wake_tx,
738 }),
739 registration,
740 worker: WorkerCustody::Running(worker),
741 })
742}
743
744fn register(
747 backend: &dyn WatchBackend,
748 targets: &WatchTargets,
749 wake: mpsc::Sender<()>,
750 stop_requested: &AtomicBool,
751 slot: &Mutex<RegistrationSlot>,
752) {
753 if stop_requested.load(Ordering::Acquire) {
754 *lock(slot) = RegistrationSlot::Cancelled;
755 return;
756 }
757 let mut source = match backend.create(targets, wake) {
758 Ok(source) => source,
759 Err(reason) => {
760 settle_error(slot, stop_requested, reason);
761 return;
762 }
763 };
764 match std::panic::catch_unwind(AssertUnwindSafe(|| source.watch(&targets.parent))) {
770 Ok(Ok(())) => {}
771 Ok(Err(reason)) => {
772 drop(source);
773 settle_error(slot, stop_requested, reason);
774 return;
775 }
776 Err(_) => {
777 std::mem::forget(source);
778 settle_failed(
779 slot,
780 "registering the notification source panicked: its event loop is not running"
781 .to_string(),
782 );
783 return;
784 }
785 }
786 {
787 let mut settled = lock(slot);
790 if !stop_requested.load(Ordering::Acquire) {
791 *settled = RegistrationSlot::Registered(Some(source));
792 return;
793 }
794 }
795 drop(source);
798 *lock(slot) = RegistrationSlot::Cancelled;
799}
800
801fn settle_error(slot: &Mutex<RegistrationSlot>, stop_requested: &AtomicBool, reason: String) {
805 if stop_requested.load(Ordering::Acquire) {
806 *lock(slot) = RegistrationSlot::Cancelled;
807 } else {
808 settle_failed(slot, reason);
809 }
810}
811
812fn settle_failed(slot: &Mutex<RegistrationSlot>, reason: String) {
813 tracing::warn!(
814 %reason,
815 "sqlite change watch notifications are unavailable; the watch runs on its sweep alone"
816 );
817 *lock(slot) = RegistrationSlot::Failed(reason);
818}
819
820#[cfg(any(test, feature = "test-support"))]
825pub mod test_support {
826 use std::collections::HashMap;
827 use std::path::{Path, PathBuf};
828 use std::sync::atomic::{AtomicUsize, Ordering};
829 use std::sync::{Arc, Condvar, Mutex, OnceLock, PoisonError, mpsc};
830 use std::time::{Duration, Instant};
831
832 use super::{WatchBackend, WatchSource, WatchTargets, lock};
833
834 static HOLDS: OnceLock<Mutex<HashMap<PathBuf, Arc<Hold>>>> = OnceLock::new();
836
837 fn holds() -> &'static Mutex<HashMap<PathBuf, Arc<Hold>>> {
838 HOLDS.get_or_init(Mutex::default)
839 }
840
841 #[derive(Default)]
842 struct Hold {
843 gate: Mutex<Gate>,
844 changed: Condvar,
845 live_sources: AtomicUsize,
846 dropped_sources: AtomicUsize,
847 }
848
849 #[derive(Default)]
850 struct Gate {
851 released: bool,
852 entered: usize,
853 }
854
855 pub struct HeldRegistrations {
858 directory: PathBuf,
859 hold: Arc<Hold>,
860 }
861
862 pub fn hold_registrations_in(directory: &Path) -> std::io::Result<HeldRegistrations> {
868 let directory = directory.canonicalize()?;
869 let hold = Arc::new(Hold::default());
870 let mut holds = lock(holds());
871 if holds.contains_key(&directory) {
872 return Err(std::io::Error::new(
873 std::io::ErrorKind::AlreadyExists,
874 format!("{} is already held", directory.display()),
875 ));
876 }
877 holds.insert(directory.clone(), Arc::clone(&hold));
878 Ok(HeldRegistrations { directory, hold })
879 }
880
881 impl HeldRegistrations {
882 pub fn wait_entered(&self, count: usize, bound: Duration) -> bool {
885 let deadline = Instant::now() + bound;
886 let mut gate = lock(&self.hold.gate);
887 while gate.entered < count {
888 let Some(left) = deadline.checked_duration_since(Instant::now()) else {
889 return false;
890 };
891 gate = self
892 .hold
893 .changed
894 .wait_timeout(gate, left)
895 .unwrap_or_else(PoisonError::into_inner)
896 .0;
897 }
898 true
899 }
900
901 pub fn release(&self) {
904 lock(&self.hold.gate).released = true;
905 self.hold.changed.notify_all();
906 }
907
908 pub fn live_sources(&self) -> usize {
910 self.hold.live_sources.load(Ordering::SeqCst)
911 }
912
913 pub fn dropped_sources(&self) -> usize {
915 self.hold.dropped_sources.load(Ordering::SeqCst)
916 }
917 }
918
919 impl Drop for HeldRegistrations {
920 fn drop(&mut self) {
921 lock(holds()).remove(&self.directory);
922 self.release();
923 }
924 }
925
926 pub(super) fn held_backend(parent: &Path) -> Option<Box<dyn WatchBackend>> {
928 let hold = lock(holds()).get(parent).cloned()?;
929 Some(Box::new(HeldBackend(hold)))
930 }
931
932 struct HeldBackend(Arc<Hold>);
933
934 impl WatchBackend for HeldBackend {
935 fn create(
936 &self,
937 _targets: &WatchTargets,
938 wake: mpsc::Sender<()>,
939 ) -> Result<Box<dyn WatchSource>, String> {
940 self.0.live_sources.fetch_add(1, Ordering::SeqCst);
941 Ok(Box::new(HeldSource {
942 hold: Arc::clone(&self.0),
943 _wake: wake,
944 }))
945 }
946 }
947
948 struct HeldSource {
949 hold: Arc<Hold>,
950 _wake: mpsc::Sender<()>,
952 }
953
954 impl WatchSource for HeldSource {
955 fn watch(&mut self, _directory: &Path) -> Result<(), String> {
956 let mut gate = lock(&self.hold.gate);
957 gate.entered += 1;
958 self.hold.changed.notify_all();
959 while !gate.released {
960 gate = self
961 .hold
962 .changed
963 .wait(gate)
964 .unwrap_or_else(PoisonError::into_inner);
965 }
966 Ok(())
967 }
968 }
969
970 impl Drop for HeldSource {
971 fn drop(&mut self) {
972 self.hold.live_sources.fetch_sub(1, Ordering::SeqCst);
973 self.hold.dropped_sources.fetch_add(1, Ordering::SeqCst);
974 }
975 }
976}
977
978#[cfg(test)]
979#[allow(clippy::expect_used, clippy::unwrap_used)]
980mod tests {
981 use super::*;
982 use std::sync::atomic::AtomicUsize;
983
984 fn settled_registration(watch: &SqliteChangeWatch) -> SqliteWatchRegistration {
986 let deadline = Instant::now() + HANG_GUARD;
987 loop {
988 let registration = watch.registration();
989 if registration != SqliteWatchRegistration::Pending || Instant::now() >= deadline {
990 return registration;
991 }
992 thread::sleep(Duration::from_millis(2));
993 }
994 }
995
996 fn retained_in(directory: &Path) -> Vec<SqliteWatchRetainedRegistration> {
997 retained_registrations()
998 .into_iter()
999 .filter(|retained| retained.directory == directory)
1000 .collect()
1001 }
1002
1003 fn assert_database_write_ticks_changed(db: &Path) {
1004 std::fs::write(db, b"").unwrap();
1005 let (tick_tx, tick_rx) = mpsc::channel();
1006 let watch = start_sqlite_change_watch(db, "test-watch", SQLITE_WATCH_SWEEP, move |tick| {
1007 let _ = tick_tx.send(tick);
1008 SqliteWatchControl::Handled
1009 })
1010 .expect("watch starts");
1011 assert_eq!(
1014 settled_registration(&watch),
1015 SqliteWatchRegistration::Registered
1016 );
1017 std::fs::write(sidecar_path(db, "-wal"), b"frame").unwrap();
1018 let tick = tick_rx
1019 .recv_timeout(Duration::from_secs(10))
1020 .expect("the write is observed within the hang guard");
1021 assert_eq!(tick, SqliteWatchTick::Changed);
1022 }
1023
1024 #[test]
1025 fn a_write_to_the_database_ticks_changed() {
1026 let dir = tempfile::tempdir().unwrap();
1027 assert_database_write_ticks_changed(&dir.path().join("store.sqlite3"));
1028 }
1029
1030 #[cfg(unix)]
1031 #[test]
1032 fn symlinked_directory_targets_match_canonical_events_for_missing_files() {
1033 let dir = tempfile::tempdir().unwrap();
1034 let actual = dir.path().join("actual");
1035 let alias = dir.path().join("alias");
1036 std::fs::create_dir(&actual).unwrap();
1037 std::os::unix::fs::symlink(&actual, &alias).unwrap();
1038 let db = alias.join("store.sqlite3");
1039 let (parent, watched_paths) = sqlite_watch_targets(&db).unwrap();
1040 let canonical_parent = actual.canonicalize().unwrap();
1041 assert_eq!(parent, canonical_parent);
1042 assert_eq!(
1043 watched_paths,
1044 sqlite_watch_paths(&canonical_parent.join("store.sqlite3"))
1045 );
1046 assert!(!db.exists());
1047 for path in &watched_paths {
1048 assert!(!path.exists());
1049 for kind in [
1050 notify::EventKind::Create(notify::event::CreateKind::File),
1051 notify::EventKind::Remove(notify::event::RemoveKind::File),
1052 ] {
1053 let event = notify::Event::new(kind).add_path(path.clone());
1054 assert!(sqlite_watch_event_relevant(&event, &parent, &watched_paths));
1055 }
1056 }
1057 for path in [
1058 canonical_parent.join("other.sqlite3-wal"),
1059 canonical_parent.join("other").join("store.sqlite3-wal"),
1060 ] {
1061 let event = notify::Event::new(notify::EventKind::Any).add_path(path);
1062 assert!(!sqlite_watch_event_relevant(
1063 &event,
1064 &parent,
1065 &watched_paths
1066 ));
1067 }
1068 let access = notify::Event::new(notify::EventKind::Access(notify::event::AccessKind::Any))
1069 .add_path(watched_paths[0].clone());
1070 assert!(!sqlite_watch_event_relevant(
1071 &access,
1072 &parent,
1073 &watched_paths
1074 ));
1075 }
1076
1077 #[cfg(unix)]
1078 #[test]
1079 fn a_write_through_a_symlinked_directory_ticks_changed() {
1080 let dir = tempfile::tempdir().unwrap();
1081 let actual = dir.path().join("actual");
1082 let alias = dir.path().join("alias");
1083 std::fs::create_dir(&actual).unwrap();
1084 std::os::unix::fs::symlink(&actual, &alias).unwrap();
1085 assert_database_write_ticks_changed(&alias.join("store.sqlite3"));
1086 }
1087
1088 #[test]
1089 fn the_sweep_ticks_without_a_notification() {
1090 let dir = tempfile::tempdir().unwrap();
1091 let db = dir.path().join("store.sqlite3");
1092 let (tick_tx, tick_rx) = mpsc::channel();
1093 let _watch =
1094 start_sqlite_change_watch(&db, "test-sweep", Duration::from_millis(50), move |tick| {
1095 let _ = tick_tx.send(tick);
1096 SqliteWatchControl::Handled
1097 })
1098 .expect("watch starts");
1099 let tick = tick_rx
1100 .recv_timeout(Duration::from_secs(10))
1101 .expect("the sweep ticks within the hang guard");
1102 assert_eq!(tick, SqliteWatchTick::Sweep);
1103 }
1104
1105 #[test]
1106 fn stop_ends_the_worker() {
1107 let dir = tempfile::tempdir().unwrap();
1108 let db = dir.path().join("store.sqlite3");
1109 let (tick_tx, tick_rx) = mpsc::channel();
1110 let _watch =
1111 start_sqlite_change_watch(&db, "test-stop", Duration::from_millis(20), move |tick| {
1112 let _ = tick_tx.send(tick);
1113 SqliteWatchControl::Stop
1114 })
1115 .expect("watch starts");
1116 tick_rx
1117 .recv_timeout(Duration::from_secs(10))
1118 .expect("one tick");
1119 assert!(
1120 tick_rx.recv_timeout(Duration::from_millis(500)).is_err(),
1121 "no tick after Stop: the worker ended (its sender is gone)"
1122 );
1123 }
1124
1125 fn wal_database(dir: &Path) -> PathBuf {
1127 let db = dir.join("store.sqlite3");
1128 let conn = crate::open(&db, crate::ConnectionProfile::PRIMARY).unwrap();
1129 conn.execute_batch("CREATE TABLE t (v INTEGER); INSERT INTO t (v) VALUES (1);")
1130 .unwrap();
1131 db
1132 }
1133
1134 struct TickProbe {
1137 ticks: Arc<AtomicUsize>,
1138 returned: mpsc::Sender<()>,
1139 }
1140
1141 impl TickProbe {
1142 fn tick(&self) {
1143 self.ticks.fetch_add(1, Ordering::SeqCst);
1144 }
1145 }
1146
1147 impl Drop for TickProbe {
1148 fn drop(&mut self) {
1149 let _ = self.returned.send(());
1150 }
1151 }
1152
1153 #[test]
1154 fn stop_and_join_waits_for_the_tick_in_flight() {
1155 let dir = tempfile::tempdir().unwrap();
1156 let db = wal_database(dir.path());
1157 let (entered_tx, entered_rx) = mpsc::channel();
1158 let (release_tx, release_rx) = mpsc::channel::<()>();
1159 let finished = Arc::new(AtomicBool::new(false));
1160 let tick_finished = Arc::clone(&finished);
1161 let tick_db = db.clone();
1162 let mut watch = start_sqlite_change_watch(
1163 &db,
1164 "test-held-tick",
1165 Duration::from_millis(20),
1166 move |_tick| {
1167 if tick_finished.load(Ordering::SeqCst) {
1168 return SqliteWatchControl::Handled;
1169 }
1170 let conn = crate::open(&tick_db, crate::ConnectionProfile::ReadOnly).unwrap();
1173 let _ = entered_tx.send(());
1174 let _ = release_rx.recv();
1175 let value: i64 = conn
1176 .query_row("SELECT v FROM t", [], |row| row.get(0))
1177 .unwrap();
1178 assert_eq!(value, 1);
1179 drop(conn);
1180 tick_finished.store(true, Ordering::SeqCst);
1181 SqliteWatchControl::Handled
1182 },
1183 )
1184 .expect("watch starts");
1185 entered_rx
1186 .recv_timeout(Duration::from_secs(10))
1187 .expect("a tick is held within the hang guard");
1188
1189 let (stopping_tx, stopping_rx) = mpsc::channel();
1190 let stopper_finished = Arc::clone(&finished);
1191 let stopper = thread::spawn(move || {
1192 let _ = stopping_tx.send(());
1193 let outcome = watch.stop_and_join();
1194 (outcome, stopper_finished.load(Ordering::SeqCst))
1195 });
1196 stopping_rx.recv().unwrap();
1197 release_tx.send(()).unwrap();
1198 let (outcome, finished_at_return) = stopper.join().unwrap();
1199
1200 assert!(
1201 finished_at_return,
1202 "stop_and_join returned while a tick still held its connection"
1203 );
1204 assert_eq!(outcome, SqliteWatchJoin::Joined);
1205 std::fs::remove_dir_all(dir.path()).unwrap();
1208 assert!(!dir.path().exists());
1209 }
1210
1211 #[test]
1212 fn stop_and_join_on_the_worker_thread_is_unjoined_and_keeps_custody() {
1213 let dir = tempfile::tempdir().unwrap();
1214 let db = dir.path().join("store.sqlite3");
1215 let custody: Arc<std::sync::Mutex<Option<SqliteChangeWatch>>> = Arc::default();
1216 let tick_custody = Arc::clone(&custody);
1217 let (outcome_tx, outcome_rx) = mpsc::channel();
1218 let watch = start_sqlite_change_watch(
1219 &db,
1220 "test-self-stop",
1221 Duration::from_millis(20),
1222 move |_| {
1223 let outcome = tick_custody
1224 .lock()
1225 .unwrap()
1226 .as_mut()
1227 .map(SqliteChangeWatch::stop_and_join);
1228 if let Some(outcome) = outcome {
1229 let _ = outcome_tx.send(outcome);
1230 }
1231 SqliteWatchControl::Handled
1232 },
1233 )
1234 .expect("watch starts");
1235 *custody.lock().unwrap() = Some(watch);
1236
1237 let on_worker = outcome_rx
1238 .recv_timeout(Duration::from_secs(10))
1239 .expect("a tick stops its own watch within the hang guard");
1240 assert_eq!(on_worker, SqliteWatchJoin::UnjoinedOnWorkerThread);
1241
1242 let mut watch = custody
1245 .lock()
1246 .unwrap()
1247 .take()
1248 .expect("the watch stays in custody");
1249 assert!(!watch.on_worker_thread());
1250 assert_eq!(watch.stop_and_join(), SqliteWatchJoin::Joined);
1251 assert!(watch.is_finished());
1252 assert_eq!(
1253 watch.stop_and_join(),
1254 SqliteWatchJoin::Joined,
1255 "a repeated stop reports the recorded join"
1256 );
1257 }
1258
1259 #[test]
1260 fn dropping_the_watch_requests_the_stop_without_blocking() {
1261 let dir = tempfile::tempdir().unwrap();
1262 let db = dir.path().join("store.sqlite3");
1263 let (entered_tx, entered_rx) = mpsc::channel();
1264 let (release_tx, release_rx) = mpsc::channel::<()>();
1265 let (returned_tx, returned_rx) = mpsc::channel();
1266 let ticks = Arc::new(AtomicUsize::new(0));
1267 let probe = TickProbe {
1268 ticks: Arc::clone(&ticks),
1269 returned: returned_tx,
1270 };
1271 let watch =
1272 start_sqlite_change_watch(&db, "test-drop", Duration::from_millis(20), move |_| {
1273 probe.tick();
1274 let _ = entered_tx.send(());
1275 let _ = release_rx.recv();
1276 SqliteWatchControl::Handled
1277 })
1278 .expect("watch starts");
1279 entered_rx
1280 .recv_timeout(Duration::from_secs(10))
1281 .expect("a tick is held within the hang guard");
1282
1283 drop(watch);
1286 release_tx.send(()).unwrap();
1287 returned_rx
1288 .recv_timeout(Duration::from_secs(10))
1289 .expect("the worker returns after the held tick");
1290 assert_eq!(
1291 ticks.load(Ordering::SeqCst),
1292 1,
1293 "no tick starts after the stop request"
1294 );
1295 }
1296
1297 const HELD_BOUND: Duration = Duration::from_secs(5);
1300 const HANG_GUARD: Duration = Duration::from_secs(10);
1301
1302 #[test]
1303 fn a_held_registration_does_not_hold_the_start() {
1304 let dir = tempfile::tempdir().unwrap();
1305 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1306 let db = dir.path().join("store.sqlite3");
1307 let (started_tx, started_rx) = mpsc::channel();
1308 let starter = thread::spawn(move || {
1309 let watch =
1310 start_sqlite_change_watch(&db, "test-held-start", SQLITE_WATCH_SWEEP, |_| {
1311 SqliteWatchControl::Handled
1312 });
1313 let _ = started_tx.send(());
1314 watch
1315 });
1316 assert!(
1317 held.wait_entered(1, HANG_GUARD),
1318 "the registration reached its platform call"
1319 );
1320 let started = started_rx.recv_timeout(HELD_BOUND);
1321 held.release();
1322 let watch = starter.join().unwrap().expect("watch starts");
1323 assert_eq!(
1324 started,
1325 Ok(()),
1326 "the start waited for the held notification registration"
1327 );
1328 drop(watch);
1329 }
1330
1331 #[test]
1332 fn the_sweep_ticks_while_the_registration_is_held() {
1333 let dir = tempfile::tempdir().unwrap();
1334 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1335 let db = dir.path().join("store.sqlite3");
1336 let (tick_tx, tick_rx) = mpsc::channel();
1337 let starter = thread::spawn(move || {
1338 start_sqlite_change_watch(
1339 &db,
1340 "test-held-sweep",
1341 Duration::from_millis(20),
1342 move |tick| {
1343 let _ = tick_tx.send(tick);
1344 SqliteWatchControl::Handled
1345 },
1346 )
1347 });
1348 assert!(
1349 held.wait_entered(1, HANG_GUARD),
1350 "the registration reached its platform call"
1351 );
1352 let tick = tick_rx.recv_timeout(HELD_BOUND);
1353 held.release();
1354 let watch = starter.join().unwrap().expect("watch starts");
1355 assert_eq!(
1356 tick,
1357 Ok(SqliteWatchTick::Sweep),
1358 "no sweep ran while the notification registration was held"
1359 );
1360 drop(watch);
1361 }
1362
1363 struct FailingBackend;
1366
1367 struct FailingSource;
1368
1369 impl WatchBackend for FailingBackend {
1370 fn create(
1371 &self,
1372 _targets: &WatchTargets,
1373 _wake: mpsc::Sender<()>,
1374 ) -> Result<Box<dyn WatchSource>, String> {
1375 Ok(Box::new(FailingSource))
1376 }
1377 }
1378
1379 impl WatchSource for FailingSource {
1380 fn watch(&mut self, directory: &Path) -> Result<(), String> {
1381 Err(format!(
1382 "cannot watch {}: the watch limit is reached",
1383 directory.display()
1384 ))
1385 }
1386 }
1387
1388 #[test]
1389 fn a_failed_registration_keeps_the_sweep() {
1390 let dir = tempfile::tempdir().unwrap();
1391 let db = dir.path().join("store.sqlite3");
1392 let (tick_tx, tick_rx) = mpsc::channel();
1393 let watch = start_with_backend(
1394 &db,
1395 "test-failed-registration",
1396 Duration::from_millis(20),
1397 move |tick| {
1398 let _ = tick_tx.send(tick);
1399 SqliteWatchControl::Handled
1400 },
1401 |_| Box::new(FailingBackend),
1402 )
1403 .expect("a failed notification registration leaves the watch on its sweep");
1404 assert_eq!(tick_rx.recv_timeout(HANG_GUARD), Ok(SqliteWatchTick::Sweep));
1405 drop(watch);
1406 }
1407
1408 #[test]
1409 fn stopping_cancels_a_held_registration_and_keeps_its_thread() {
1410 let dir = tempfile::tempdir().unwrap();
1411 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1412 let directory = dir.path().canonicalize().unwrap();
1413 let db = dir.path().join("store.sqlite3");
1414 let mut watch =
1415 start_sqlite_change_watch(&db, "test-held-stop", Duration::from_millis(20), |_| {
1416 SqliteWatchControl::Handled
1417 })
1418 .expect("watch starts");
1419 assert!(
1420 held.wait_entered(1, HANG_GUARD),
1421 "the registration reached its platform call"
1422 );
1423
1424 assert_eq!(watch.stop_and_join(), SqliteWatchJoin::Joined);
1426 assert_eq!(watch.registration(), SqliteWatchRegistration::Pending);
1427 assert_eq!(held.live_sources(), 1);
1428
1429 held.release();
1432 assert_eq!(
1433 settled_registration(&watch),
1434 SqliteWatchRegistration::Cancelled
1435 );
1436 assert_eq!((held.live_sources(), held.dropped_sources()), (0, 1));
1437 assert!(retained_in(&directory).is_empty());
1439 assert_eq!(watch.stop_and_join(), SqliteWatchJoin::Joined);
1440 }
1441
1442 #[test]
1443 fn dropping_a_watch_retains_its_held_registration_until_it_returns() {
1444 let dir = tempfile::tempdir().unwrap();
1445 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1446 let directory = dir.path().canonicalize().unwrap();
1447 let db = dir.path().join("store.sqlite3");
1448 let watch =
1449 start_sqlite_change_watch(&db, "test-held-drop", Duration::from_millis(20), |_| {
1450 SqliteWatchControl::Handled
1451 })
1452 .expect("watch starts");
1453 assert!(
1454 held.wait_entered(1, HANG_GUARD),
1455 "the registration reached its platform call"
1456 );
1457
1458 drop(watch);
1461 let retained = retained_in(&directory);
1462 assert_eq!(retained.len(), 1, "{retained:?}");
1463 assert_eq!(retained[0].thread_name, "test-held-drop-register");
1464 assert_eq!(held.live_sources(), 1);
1465
1466 held.release();
1469 let deadline = Instant::now() + HANG_GUARD;
1470 while !retained_in(&directory).is_empty() && Instant::now() < deadline {
1471 thread::sleep(Duration::from_millis(2));
1472 }
1473 assert!(
1474 retained_in(&directory).is_empty(),
1475 "the process joins a retained registration once it returns"
1476 );
1477 assert_eq!((held.live_sources(), held.dropped_sources()), (0, 1));
1478 }
1479
1480 #[test]
1481 fn a_pending_registration_is_described_as_the_process_retains_it() {
1482 let dir = tempfile::tempdir().unwrap();
1483 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1484 let directory = dir.path().canonicalize().unwrap();
1485 let db = dir.path().join("store.sqlite3");
1486 let watch =
1487 start_sqlite_change_watch(&db, "test-held-pending", Duration::from_millis(20), |_| {
1488 SqliteWatchControl::Handled
1489 })
1490 .expect("watch starts");
1491 assert!(
1492 held.wait_entered(1, HANG_GUARD),
1493 "the registration reached its platform call"
1494 );
1495
1496 watch.request_stop();
1500 let pending = watch
1501 .pending_registration()
1502 .expect("a held registration is pending");
1503 assert_eq!(pending.directory, directory);
1504 assert_eq!(pending.thread_name, "test-held-pending-register");
1505 drop(watch);
1506 let retained = retained_in(&directory);
1507 assert_eq!(retained.len(), 1, "{retained:?}");
1508 assert_eq!(
1509 (&retained[0].directory, &retained[0].thread_name),
1510 (&pending.directory, &pending.thread_name)
1511 );
1512
1513 held.release();
1514 let deadline = Instant::now() + HANG_GUARD;
1515 while !retained_in(&directory).is_empty() && Instant::now() < deadline {
1516 thread::sleep(Duration::from_millis(2));
1517 }
1518 assert!(retained_in(&directory).is_empty());
1519 }
1520
1521 #[test]
1522 fn a_settled_registration_is_not_pending() {
1523 let dir = tempfile::tempdir().unwrap();
1524 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1527 held.release();
1528 let db = dir.path().join("store.sqlite3");
1529 let watch =
1530 start_sqlite_change_watch(&db, "test-settled-pending", SQLITE_WATCH_SWEEP, |_| {
1531 SqliteWatchControl::Handled
1532 })
1533 .expect("watch starts");
1534 assert_eq!(
1535 settled_registration(&watch),
1536 SqliteWatchRegistration::Registered
1537 );
1538 assert_eq!(watch.pending_registration(), None);
1539 watch.request_stop();
1540 assert_eq!(watch.pending_registration(), None);
1541 }
1542
1543 #[test]
1544 fn a_failed_registration_reports_its_reason() {
1545 let dir = tempfile::tempdir().unwrap();
1546 let db = dir.path().join("store.sqlite3");
1547 let watch = start_with_backend(
1548 &db,
1549 "test-failed-reason",
1550 SQLITE_WATCH_SWEEP,
1551 |_| SqliteWatchControl::Handled,
1552 |_| Box::new(FailingBackend),
1553 )
1554 .expect("watch starts");
1555 let registration = settled_registration(&watch);
1556 assert!(
1557 matches!(
1558 ®istration,
1559 SqliteWatchRegistration::Failed(reason)
1560 if reason.contains("the watch limit is reached")
1561 ),
1562 "{registration:?}"
1563 );
1564 }
1565
1566 const PANIC_CHILD: &str = "MEERKAT_TEST_SQLITE_WATCH_PANIC_CHILD";
1568 const PANIC_CHILD_DONE: &str = "SQLITE_WATCH_PANIC_CONTAINED";
1570
1571 struct PanickingBackend;
1575
1576 struct PanickingSource;
1577
1578 impl WatchBackend for PanickingBackend {
1579 fn create(
1580 &self,
1581 _targets: &WatchTargets,
1582 _wake: mpsc::Sender<()>,
1583 ) -> Result<Box<dyn WatchSource>, String> {
1584 Ok(Box::new(PanickingSource))
1585 }
1586 }
1587
1588 #[allow(clippy::panic)]
1589 impl WatchSource for PanickingSource {
1590 fn watch(&mut self, _directory: &Path) -> Result<(), String> {
1591 panic!("the notification event loop is gone");
1592 }
1593 }
1594
1595 #[allow(clippy::panic)]
1596 impl Drop for PanickingSource {
1597 fn drop(&mut self) {
1598 panic!("the notification event loop is gone");
1599 }
1600 }
1601
1602 #[test]
1605 fn a_panicking_registration_leaves_the_sweep_running() {
1606 if std::env::var_os(PANIC_CHILD).is_some() {
1607 let dir = tempfile::tempdir().unwrap();
1608 let db = dir.path().join("store.sqlite3");
1609 let (tick_tx, tick_rx) = mpsc::channel();
1610 let watch = start_with_backend(
1611 &db,
1612 "test-panicking-registration",
1613 Duration::from_millis(20),
1614 move |tick| {
1615 let _ = tick_tx.send(tick);
1616 SqliteWatchControl::Handled
1617 },
1618 |_| Box::new(PanickingBackend),
1619 )
1620 .expect("watch starts");
1621 let registration = settled_registration(&watch);
1622 assert!(
1623 matches!(registration, SqliteWatchRegistration::Failed(_)),
1624 "{registration:?}"
1625 );
1626 assert_eq!(tick_rx.recv_timeout(HANG_GUARD), Ok(SqliteWatchTick::Sweep));
1627 drop(watch);
1628 println!("\n{PANIC_CHILD_DONE}");
1629 return;
1630 }
1631 #[allow(clippy::disallowed_methods)] let output = std::process::Command::new(std::env::current_exe().unwrap())
1633 .args([
1634 "--exact",
1635 "watch::tests::a_panicking_registration_leaves_the_sweep_running",
1636 "--nocapture",
1637 "--test-threads=1",
1638 ])
1639 .env(PANIC_CHILD, "1")
1640 .output()
1641 .unwrap();
1642 let stdout = String::from_utf8_lossy(&output.stdout);
1643 assert!(
1644 output.status.success() && stdout.contains(PANIC_CHILD_DONE),
1645 "the watch did not survive its panicking registration ({}):\n{stdout}\n{}",
1646 output.status,
1647 String::from_utf8_lossy(&output.stderr)
1648 );
1649 }
1650
1651 const ALONE_CHILD: &str = "MEERKAT_TEST_SQLITE_WATCH_ALONE_CHILD";
1653 const ALONE_CHILD_DONE: &str = "SQLITE_WATCH_ALONE_TEST_PASSED";
1655
1656 fn alone_in_child_process(test: &str, body: impl FnOnce()) {
1663 if std::env::var_os(ALONE_CHILD).is_some() {
1664 body();
1665 println!("\n{ALONE_CHILD_DONE}");
1666 return;
1667 }
1668 #[allow(clippy::disallowed_methods)] let output = std::process::Command::new(std::env::current_exe().unwrap())
1670 .args([
1671 "--exact",
1672 &format!("watch::tests::{test}"),
1673 "--nocapture",
1674 "--test-threads=1",
1675 ])
1676 .env(ALONE_CHILD, "1")
1677 .output()
1678 .unwrap();
1679 let stdout = String::from_utf8_lossy(&output.stdout);
1680 assert!(
1681 output.status.success() && stdout.contains(ALONE_CHILD_DONE),
1682 "{test} failed alone in its child process ({}):\n{stdout}\n{}",
1683 output.status,
1684 String::from_utf8_lossy(&output.stderr)
1685 );
1686 }
1687
1688 #[test]
1689 fn registrations_past_the_cap_leave_new_watches_on_their_sweep() {
1690 alone_in_child_process(
1691 "registrations_past_the_cap_leave_new_watches_on_their_sweep",
1692 || {
1693 assert_eq!(REGISTRATIONS_IN_FLIGHT.load(Ordering::Acquire), 0);
1695 let dir = tempfile::tempdir().unwrap();
1696 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1697 let watches: Vec<_> = (0..MAX_REGISTRATIONS_IN_FLIGHT)
1698 .map(|index| {
1699 start_sqlite_change_watch(
1700 &dir.path().join(format!("store-{index}.sqlite3")),
1701 "test-cap-held",
1702 SQLITE_WATCH_SWEEP,
1703 |_| SqliteWatchControl::Handled,
1704 )
1705 .expect("watch starts")
1706 })
1707 .collect();
1708 assert!(
1709 held.wait_entered(MAX_REGISTRATIONS_IN_FLIGHT, HANG_GUARD),
1710 "every registration under the cap reached its platform call"
1711 );
1712
1713 let (tick_tx, tick_rx) = mpsc::channel();
1716 let over = start_sqlite_change_watch(
1717 &dir.path().join("store-over.sqlite3"),
1718 "test-cap-over",
1719 Duration::from_millis(20),
1720 move |tick| {
1721 let _ = tick_tx.send(tick);
1722 SqliteWatchControl::Handled
1723 },
1724 )
1725 .expect("a watch past the cap still starts");
1726 let registration = over.registration();
1727 assert!(
1728 matches!(
1729 ®istration,
1730 SqliteWatchRegistration::Failed(reason) if reason.contains("already pending")
1731 ),
1732 "{registration:?}"
1733 );
1734 assert!(
1735 over.registration.thread.is_none(),
1736 "no registration thread started past the cap"
1737 );
1738 assert_eq!(held.live_sources(), MAX_REGISTRATIONS_IN_FLIGHT);
1739 assert_eq!(tick_rx.recv_timeout(HANG_GUARD), Ok(SqliteWatchTick::Sweep));
1740
1741 held.release();
1744 for watch in &watches {
1745 assert_eq!(
1746 settled_registration(watch),
1747 SqliteWatchRegistration::Registered
1748 );
1749 }
1750 let deadline = Instant::now() + HANG_GUARD;
1751 while REGISTRATIONS_IN_FLIGHT.load(Ordering::Acquire) > 0
1752 && Instant::now() < deadline
1753 {
1754 thread::sleep(Duration::from_millis(2));
1755 }
1756 let fresh_dir = tempfile::tempdir().unwrap();
1757 let fresh = start_sqlite_change_watch(
1758 &fresh_dir.path().join("store.sqlite3"),
1759 "test-cap-fresh",
1760 SQLITE_WATCH_SWEEP,
1761 |_| SqliteWatchControl::Handled,
1762 )
1763 .expect("watch starts");
1764 assert_eq!(
1765 settled_registration(&fresh),
1766 SqliteWatchRegistration::Registered,
1767 "a fresh watch registers once every place is given back"
1768 );
1769 drop(watches);
1770 drop(over);
1771 },
1772 );
1773 }
1774
1775 #[test]
1780 fn a_held_burst_of_64_registrations_all_reach_their_platform_call() {
1781 alone_in_child_process(
1782 "a_held_burst_of_64_registrations_all_reach_their_platform_call",
1783 || {
1784 const BURST: usize = 64;
1785 assert_eq!(REGISTRATIONS_IN_FLIGHT.load(Ordering::Acquire), 0);
1787 let dir = tempfile::tempdir().unwrap();
1788 let held = test_support::hold_registrations_in(dir.path()).unwrap();
1789 let watches: Vec<_> = (0..BURST)
1790 .map(|index| {
1791 start_sqlite_change_watch(
1792 &dir.path().join(format!("store-{index}.sqlite3")),
1793 "test-burst-held",
1794 SQLITE_WATCH_SWEEP,
1795 |_| SqliteWatchControl::Handled,
1796 )
1797 .expect("watch starts")
1798 })
1799 .collect();
1800 let refused = watches
1801 .iter()
1802 .filter(|watch| {
1803 matches!(watch.registration(), SqliteWatchRegistration::Failed(_))
1804 })
1805 .count();
1806 assert_eq!(refused, 0, "registrations of the burst refused at the cap");
1807 assert!(
1808 held.wait_entered(BURST, HANG_GUARD),
1809 "every registration of the burst reached its platform call"
1810 );
1811 held.release();
1812 for watch in &watches {
1813 assert_eq!(
1814 settled_registration(watch),
1815 SqliteWatchRegistration::Registered
1816 );
1817 }
1818 },
1819 );
1820 }
1821
1822 static WARNINGS: Mutex<Vec<(Option<String>, String)>> = Mutex::new(Vec::new());
1825
1826 struct WarningCapture;
1830
1831 struct MessageField(String);
1832
1833 impl tracing::field::Visit for MessageField {
1834 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
1835 if field.name() == "message" {
1836 self.0 = format!("{value:?}");
1837 }
1838 }
1839 }
1840
1841 impl tracing::Subscriber for WarningCapture {
1842 fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool {
1843 *metadata.level() == tracing::Level::WARN
1844 }
1845
1846 fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1847 tracing::span::Id::from_u64(1)
1848 }
1849
1850 fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
1851
1852 fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
1853
1854 fn event(&self, event: &tracing::Event<'_>) {
1855 let mut message = MessageField(String::new());
1856 event.record(&mut message);
1857 lock(&WARNINGS).push((thread::current().name().map(str::to_string), message.0));
1858 }
1859
1860 fn enter(&self, _span: &tracing::span::Id) {}
1861
1862 fn exit(&self, _span: &tracing::span::Id) {}
1863 }
1864
1865 fn capture_warnings() {
1867 static INSTALLED: OnceLock<bool> = OnceLock::new();
1868 let installed = *INSTALLED
1869 .get_or_init(|| tracing::subscriber::set_global_default(WarningCapture).is_ok());
1870 assert!(
1871 installed,
1872 "the warning capture is this process's global subscriber"
1873 );
1874 }
1875
1876 fn warnings_from(thread_name: &str) -> Vec<String> {
1877 lock(&WARNINGS)
1878 .iter()
1879 .filter(|(name, _)| name.as_deref() == Some(thread_name))
1880 .map(|(_, message)| message.clone())
1881 .collect()
1882 }
1883
1884 struct GatedFailingBackend {
1887 entered: mpsc::Sender<()>,
1888 release: Mutex<Option<mpsc::Receiver<()>>>,
1889 }
1890
1891 struct GatedFailingSource {
1892 entered: mpsc::Sender<()>,
1893 release: mpsc::Receiver<()>,
1894 }
1895
1896 impl WatchBackend for GatedFailingBackend {
1897 fn create(
1898 &self,
1899 _targets: &WatchTargets,
1900 _wake: mpsc::Sender<()>,
1901 ) -> Result<Box<dyn WatchSource>, String> {
1902 let release = lock(&self.release)
1903 .take()
1904 .ok_or_else(|| "created twice".to_string())?;
1905 Ok(Box::new(GatedFailingSource {
1906 entered: self.entered.clone(),
1907 release,
1908 }))
1909 }
1910 }
1911
1912 impl WatchSource for GatedFailingSource {
1913 fn watch(&mut self, directory: &Path) -> Result<(), String> {
1914 let _ = self.entered.send(());
1915 let _ = self.release.recv();
1916 Err(format!(
1917 "cannot watch {}: the watch limit is reached",
1918 directory.display()
1919 ))
1920 }
1921 }
1922
1923 fn start_failing_after_release(
1926 dir: &Path,
1927 name: &str,
1928 ) -> (SqliteChangeWatch, mpsc::Receiver<()>, mpsc::Sender<()>) {
1929 let (entered_tx, entered_rx) = mpsc::channel();
1930 let (release_tx, release_rx) = mpsc::channel();
1931 let backend = GatedFailingBackend {
1932 entered: entered_tx,
1933 release: Mutex::new(Some(release_rx)),
1934 };
1935 let watch = start_with_backend(
1936 &dir.join("store.sqlite3"),
1937 name,
1938 SQLITE_WATCH_SWEEP,
1939 |_| SqliteWatchControl::Handled,
1940 move |_| Box::new(backend),
1941 )
1942 .expect("watch starts");
1943 (watch, entered_rx, release_tx)
1944 }
1945
1946 #[test]
1947 fn a_registration_failing_after_its_watch_stopped_is_cancelled_quietly() {
1948 capture_warnings();
1949 let live_dir = tempfile::tempdir().unwrap();
1952 let (live, live_entered, live_release) =
1953 start_failing_after_release(live_dir.path(), "test-fail-live");
1954 live_entered
1955 .recv_timeout(HANG_GUARD)
1956 .expect("the live registration reached its platform call");
1957 live_release.send(()).unwrap();
1958 assert!(matches!(
1959 settled_registration(&live),
1960 SqliteWatchRegistration::Failed(_)
1961 ));
1962 assert_eq!(warnings_from("test-fail-live-register").len(), 1);
1963
1964 let stopped_dir = tempfile::tempdir().unwrap();
1967 let (mut stopped, stopped_entered, stopped_release) =
1968 start_failing_after_release(stopped_dir.path(), "test-fail-stopped");
1969 stopped_entered
1970 .recv_timeout(HANG_GUARD)
1971 .expect("the registration reached its platform call");
1972 assert_eq!(stopped.stop_and_join(), SqliteWatchJoin::Joined);
1973 stopped_release.send(()).unwrap();
1974 assert_eq!(
1975 (
1976 settled_registration(&stopped),
1977 warnings_from("test-fail-stopped-register")
1978 ),
1979 (SqliteWatchRegistration::Cancelled, Vec::<String>::new())
1980 );
1981 }
1982}