Skip to main content

meerkat_sqlite/
watch.rs

1//! Cross-process change watch for one SQLite database file.
2//!
3//! Another process's commit to a database is invisible to this process's
4//! in-memory signals. This watch turns file-system notifications on the
5//! database, its `-wal` and `-shm` sidecars and their directory into
6//! coalesced ticks for a consumer that then reads durable state.
7//!
8//! A notification is a HINT, never a guarantee: SQLite writes WAL frames
9//! before the WAL index makes the commit visible, so a reader woken by an
10//! event can still read the old state, and no later event is promised. The
11//! watch therefore also ticks on a bounded sweep interval; the consumer's
12//! sweep read is the eventual-wake guarantee. Neither the watch nor the sweep
13//! ever defines permission, custody or expiry: correctness rests with the
14//! consumer's durable reads and its own fences.
15//!
16//! Registering for notifications is a hint too, and nothing waits for it. The
17//! operating system can hold a registration without bound (macOS FSEvents
18//! registration has taken over 30 seconds) and nothing can interrupt it, so
19//! the sweep worker starts first and the registration runs on a thread of its
20//! own: starting a watch returns at once, and a registration that fails leaves
21//! the watch on its sweep alone. [`SqliteChangeWatch::registration`] reports
22//! where it stands.
23//!
24//! This is the contract the mob event bus has used since its external watch
25//! landed, extracted so every store shares one implementation.
26//!
27//! Teardown is explicit. Dropping a watch, or [`SqliteWatchStopHandle::request_stop`],
28//! only asks the worker to return and never blocks: a tick already running
29//! finishes on its own thread, unjoined. [`SqliteChangeWatch::stop_and_join`]
30//! is the one operation that waits for the worker, and it cannot do so from
31//! the worker's own thread. Neither waits for a registration that is still
32//! running: a stop cancels it, and its thread discards its result when the
33//! platform call returns. That thread is never detached: it stays with its
34//! watch and, once the watch is dropped, with the process
35//! ([`retained_registrations`]) until it returns.
36
37use 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
46/// Default sweep interval: the bounded eventual-wake guarantee.
47pub const SQLITE_WATCH_SWEEP: Duration = Duration::from_millis(5_000);
48/// Settle delay after a notification, so a burst of events (a commit touches
49/// the WAL and its index) coalesces into one tick.
50const 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);
53/// Notification registrations a process runs at once. A stalled platform
54/// (macOS FSEvents) holds each one for as long as it stalls, with two threads
55/// apiece, so a stall holds at most 128 threads; past this many, a new watch
56/// runs on its sweep alone instead of adding two more. A watch refused at the
57/// cap never registers later: it stays on its sweep for the rest of its life,
58/// which for a store held open is the life of the process. That is still
59/// correct, since correctness rests on the sweep and a notification only
60/// wakes the consumer sooner. The cap fits the bursts of a normal startup,
61/// such as parallel store opens. A queue that registers a refused watch once
62/// a place frees was considered and closed: the cliff needs more than 64
63/// registrations in flight at once.
64const MAX_REGISTRATIONS_IN_FLIGHT: usize = 64;
65/// Registration threads running now in this process, for watches alive or
66/// dropped.
67static REGISTRATIONS_IN_FLIGHT: AtomicUsize = AtomicUsize::new(0);
68
69/// Why the consumer is being asked to look.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum SqliteWatchTick {
72    /// A relevant file-system notification arrived (coalesced).
73    Changed,
74    /// The sweep interval elapsed without a notification.
75    Sweep,
76}
77
78/// What the consumer's tick achieved.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum SqliteWatchControl {
81    /// The tick was handled.
82    Handled,
83    /// The consumer's durable read failed: ticks are suppressed until a
84    /// bounded exponential backoff elapses, so a notification storm cannot
85    /// hammer a failing store.
86    Failed,
87    /// The consumer is gone: stop the watch.
88    Stop,
89}
90
91/// How [`SqliteChangeWatch::stop_and_join`] ended.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[must_use]
94pub enum SqliteWatchJoin {
95    /// The worker returned and was joined: no tick is running and none will
96    /// run.
97    Joined,
98    /// The worker was joined after a tick panicked: no tick is running and
99    /// none will run.
100    JoinedAfterPanic,
101    /// Called on the worker's own thread, from inside a tick. The stop is
102    /// requested, but a thread cannot join itself: the worker returns after
103    /// the current tick, unjoined. The join handle stays in this watch, so
104    /// whoever holds the watch must keep it and join it from another thread.
105    /// Never a settled outcome.
106    UnjoinedOnWorkerThread,
107}
108
109/// Where a watch's notification registration stands
110/// ([`SqliteChangeWatch::registration`]). Notifications only shorten a wake:
111/// the sweep runs whatever this says.
112#[derive(Debug, Clone, PartialEq, Eq)]
113#[non_exhaustive]
114pub enum SqliteWatchRegistration {
115    /// Running on its own thread, which the operating system can hold without
116    /// bound. Until it ends, only the sweep wakes the consumer.
117    Pending,
118    /// Registered. Its notifications stop when the watch does.
119    Registered,
120    /// Failed, or its thread could not start: only the sweep wakes the
121    /// consumer.
122    Failed(String),
123    /// The watch stopped before the registration ended; its thread discarded
124    /// the result.
125    Cancelled,
126}
127
128/// Asks a watch's worker to return, without owning the worker. Cheap to clone;
129/// never blocks and never joins.
130#[derive(Debug, Clone)]
131pub struct SqliteWatchStopHandle {
132    control: Arc<WatchControl>,
133}
134
135impl SqliteWatchStopHandle {
136    /// Ask the worker to return. Once it observes the request it starts no
137    /// further tick; a tick already running finishes first. Never blocks.
138    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        // Wake a worker waiting for its next tick. A worker that already
153        // returned has dropped the receiver; nothing is left to wake.
154        let _ = self.wake.send(());
155    }
156}
157
158/// Join custody of a watch's worker thread.
159enum WorkerCustody {
160    Running(thread::JoinHandle<()>),
161    Joined(SqliteWatchJoin),
162}
163
164/// A registration's result, shared by its thread and its watch.
165enum RegistrationSlot {
166    Pending,
167    /// Registered; the source until the watch stops.
168    Registered(Option<Box<dyn WatchSource>>),
169    Failed(String),
170    Cancelled,
171}
172
173/// One running registration's place among [`MAX_REGISTRATIONS_IN_FLIGHT`],
174/// given back when its thread ends, or when the thread never starts.
175struct InFlightPlace;
176
177impl InFlightPlace {
178    /// Take a place, or say how many registrations hold them all.
179    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
194/// A registration thread and what it registers.
195struct 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
212/// Custody of a watch's notification registration: its result, and its
213/// thread until that is joined.
214struct RegistrationCustody {
215    slot: Arc<Mutex<RegistrationSlot>>,
216    /// `None` once joined, or when the thread never started.
217    thread: Option<RegistrationThread>,
218}
219
220impl RegistrationCustody {
221    /// Register `targets` on a thread of its own, named after the watch. A
222    /// thread that cannot start, or a process already running
223    /// [`MAX_REGISTRATIONS_IN_FLIGHT`] registrations, leaves the registration
224    /// failed: the sweep alone wakes the consumer, for the rest of the
225    /// watch's life.
226    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            // Given back as the thread ends, however it ends.
253            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    /// The registration's thread while the registration is pending. Its
292    /// thread settles the slot before it ends, so a pending registration's
293    /// thread is running.
294    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    /// Stop the notifications, once the stop flag is set: drop a registered
302    /// source, and join the thread if it has returned. A registration still
303    /// running discards its result when it ends.
304    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    /// The watch is going: stop, and hand a thread still running to the
321    /// process's custody.
322    fn release(&mut self) {
323        self.stop();
324        if let Some(thread) = self.thread.take() {
325            retain_registration(thread);
326        }
327    }
328}
329
330/// A running watch: custody of its worker thread and of its notification
331/// registration.
332///
333/// Dropping it requests the stop and never joins its worker: the worker
334/// returns at its next wake, after any tick that is running, unjoined. Use
335/// [`Self::stop_and_join`] to wait for the worker.
336pub 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    /// A handle that can request this watch's stop after the watch itself has
350    /// moved elsewhere.
351    pub fn stop_handle(&self) -> SqliteWatchStopHandle {
352        SqliteWatchStopHandle {
353            control: Arc::clone(&self.control),
354        }
355    }
356
357    /// Ask the worker to return; see [`SqliteWatchStopHandle::request_stop`].
358    pub fn request_stop(&self) {
359        self.control.request_stop();
360    }
361
362    /// Whether the calling thread is this watch's worker, i.e. the caller is
363    /// running inside one of its ticks.
364    pub fn on_worker_thread(&self) -> bool {
365        self.worker_thread_id() == Some(thread::current().id())
366    }
367
368    /// The worker thread's identity until the worker is joined.
369    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    /// Whether the worker has returned (joined or not). A returned worker
377    /// runs no tick again.
378    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    /// Where the notification registration stands. The sweep runs whatever
386    /// it says.
387    pub fn registration(&self) -> SqliteWatchRegistration {
388        self.registration.state()
389    }
390
391    /// The notification registration this watch would leave with the
392    /// process if it were dropped now, described as
393    /// [`retained_registrations`] would list it: `Some` exactly while
394    /// [`Self::registration`] is [`SqliteWatchRegistration::Pending`].
395    ///
396    /// Read after a stop request, it names the registration the stop
397    /// cancelled while the operating system still held it: that thread
398    /// discards the result when the platform call returns, and until then
399    /// stays with this watch or, once the watch is dropped, with the
400    /// process.
401    pub fn pending_registration(&self) -> Option<SqliteWatchRetainedRegistration> {
402        self.registration.pending()
403    }
404
405    /// Request the stop, stop the notifications, and wait for the worker to
406    /// return: a tick that is running finishes first. Unbounded by design: it
407    /// waits exactly as long as that tick runs.
408    ///
409    /// It never waits for a notification registration that is still running:
410    /// the stop cancels it, and its thread stays in this watch until it
411    /// returns ([`Self::registration`] says
412    /// [`SqliteWatchRegistration::Pending`] until then). A later call joins
413    /// that thread once it has.
414    ///
415    /// On the worker's own thread it cannot wait; it returns
416    /// [`SqliteWatchJoin::UnjoinedOnWorkerThread`] and keeps the join handle
417    /// in this watch, so a later call from another thread still joins.
418    /// Calling it again after a join returns the recorded outcome.
419    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        // Never wait here: the last owner can drop a watch on any thread,
443        // including its own worker's. A registration still running goes to
444        // the process's custody.
445        self.control.request_stop();
446        self.registration.release();
447    }
448}
449
450/// Registration threads still running after their watch was dropped.
451static 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/// A notification registration still running after its watch was dropped
458/// ([`retained_registrations`]), or one a live watch would leave with the
459/// process ([`SqliteChangeWatch::pending_registration`]).
460#[derive(Debug, Clone, PartialEq, Eq)]
461#[non_exhaustive]
462pub struct SqliteWatchRetainedRegistration {
463    /// The directory it registers.
464    pub directory: PathBuf,
465    /// The name of its thread.
466    pub thread_name: String,
467    /// How long it has been running.
468    pub running_for: Duration,
469}
470
471/// The notification registrations this process still holds for watches that
472/// are gone. Each one is blocked in the operating system, which nothing can
473/// interrupt, and discards its result when that call returns. Joins the ones
474/// that have returned; never waits.
475pub 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
487/// Join the retained threads that have returned.
488fn 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
504/// The database file and its sidecars.
505pub 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    // FSEvents reports canonical paths, including /private/var for /var on
528    // macOS. Resolve only the existing directory: database and sidecar files
529    // may not exist yet, or may have been removed when an event is delivered.
530    let watched_paths = sqlite_watch_paths(&parent.join(file_name));
531    Ok((parent, watched_paths))
532}
533
534/// Whether a notification concerns the database (its files or directory).
535pub fn sqlite_watch_event_relevant(
536    event: &notify::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
556/// The directory a watch registers, and the database files whose changes in
557/// it wake the worker.
558struct WatchTargets {
559    parent: PathBuf,
560    watched_paths: Vec<PathBuf>,
561}
562
563/// Creates a watch's notification source: notify's recommended watcher, or a
564/// held fake under the `test-support` feature.
565trait WatchBackend: Send {
566    /// A source that sends on `wake` for each relevant change once it is
567    /// registered.
568    fn create(
569        &self,
570        targets: &WatchTargets,
571        wake: mpsc::Sender<()>,
572    ) -> Result<Box<dyn WatchSource>, String>;
573}
574
575/// A created notification source. Dropping it stops its notifications.
576trait WatchSource: Send {
577    /// Register `directory`. Returns once the platform has registered it,
578    /// which has no bound: macOS FSEvents registration can take tens of
579    /// seconds.
580    fn watch(&mut self, directory: &Path) -> Result<(), String>;
581}
582
583/// notify's recommended watcher: FSEvents on macOS, inotify on Linux.
584struct 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                    // The sweep bounds anything a watcher error lost.
602                    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/// The backend that registers a watch of `parent`: notify's, unless test
622/// support holds the registrations in that directory.
623#[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
636/// Start watching `db_path`. `on_tick` runs on a dedicated thread named
637/// `thread_name`, once per coalesced notification and once per `sweep`
638/// without one, until the watch is stopped or `on_tick` returns
639/// [`SqliteWatchControl::Stop`].
640///
641/// Returns as soon as that worker runs. The notification registration runs
642/// on a thread of its own, `<thread_name>-register`, which ends once the
643/// platform has registered (at once with inotify);
644/// [`SqliteChangeWatch::registration`] reports it. While the platform holds
645/// registrations, each one holds that thread, and on macOS notify's run-loop
646/// thread too, until the platform call returns. At most 64 run at once in a
647/// process: a watch started while 64 are pending runs on its sweep alone
648/// (`Failed`) for the rest of its life and logs a warning. Errors only when
649/// the database's directory cannot be resolved or the worker cannot start.
650pub 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    // The sweep is the guarantee: its worker starts first, before anything
677    // that can block.
678    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                    // The watcher (and every sender) is gone: stop.
691                    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                // A stop requested while the burst settled still precedes
701                // this tick.
702                if worker_stop_requested.load(Ordering::Acquire) {
703                    break;
704                }
705                if recovery_deadline.is_some_and(|deadline| Instant::now() < deadline) {
706                    // A notification storm must not bypass a failed read's
707                    // recovery deadline.
708                    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
744/// The registration thread: create the source and register it, then hand it
745/// to the watch, or discard it when the watch stopped meanwhile.
746fn 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    // notify 8.2.0's inotify watcher ignores a failed start of its event-loop
765    // thread; registering then panics, and so would its drop, aborting the
766    // process. Contain the panic and leak that source (a sender and a waker)
767    // instead of dropping it. An error, unlike a panic, comes from a live
768    // event loop, so its source drops normally.
769    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        // The stop side sets its flag before it reads the slot, so a flag
788        // read under the slot lock never leaves a source live past a stop.
789        let mut settled = lock(slot);
790        if !stop_requested.load(Ordering::Acquire) {
791            *settled = RegistrationSlot::Registered(Some(source));
792            return;
793        }
794    }
795    // The watch stopped while the platform registered: discard the source
796    // here, so its notifications never start, then settle.
797    drop(source);
798    *lock(slot) = RegistrationSlot::Cancelled;
799}
800
801/// Settle a registration that returned an error: failed, with a warning, while
802/// its watch runs on its sweep alone; cancelled, quietly, once the watch has
803/// stopped, since nothing is left to run on the sweep.
804fn 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/// Held watch registrations, for tests of code that starts SQLite change
821/// watches. Every watch started on a database in a held directory blocks in
822/// its notification registration, as a stalled platform registration (macOS
823/// FSEvents) does, until the hold is released.
824#[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    /// Held directories, by canonical path.
835    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    /// Holds the notification registrations of the watches on databases in
856    /// one directory. Dropping it releases them and ends the hold.
857    pub struct HeldRegistrations {
858        directory: PathBuf,
859        hold: Arc<Hold>,
860    }
861
862    /// Hold the registration of every watch started from now on for a
863    /// database in `directory` (an existing directory, matched by its
864    /// canonical path), until the returned hold is released or dropped. A
865    /// held registration has created its source and blocks in registering
866    /// it; once released it registers, without ever notifying.
867    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        /// Wait, at most `bound`, until `count` registrations have entered
883        /// the hold. Whether they had.
884        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        /// Let every held registration register, and every later one in the
902        /// directory register at once.
903        pub fn release(&self) {
904            lock(&self.hold.gate).released = true;
905            self.hold.changed.notify_all();
906        }
907
908        /// Sources created through this hold and not dropped yet.
909        pub fn live_sources(&self) -> usize {
910            self.hold.live_sources.load(Ordering::SeqCst)
911        }
912
913        /// Sources created through this hold and dropped since.
914        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    /// The held backend for a watch of `parent`, while a hold covers it.
927    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        /// Kept for the source's life, as a real source keeps its wake.
951        _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    /// Wait, within the hang guard, until the watch's registration ends.
985    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        // Notifications are registered after the start returns: a write
1012        // before that is seen by the sweep alone.
1013        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    /// A WAL database holding one row, as the stores keep theirs.
1126    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    /// Counts ticks, and reports once when the worker drops its tick closure,
1135    /// which it does as it returns.
1136    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                // The held tick keeps a read connection open, as a store
1171                // catch-up does.
1172                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        // The joined worker can never open the database again, so nothing
1206        // recreates a file while the directory goes: no sleep, no retry.
1207        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        // The self-stop kept the join handle, so from here the worker is
1243        // really joined rather than reported as gone.
1244        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        // Dropping never waits for the held tick: this thread releases the
1284        // tick only after the drop returned.
1285        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    /// How long a held registration may keep a start from returning, or the
1298    /// sweep from ticking, before the test calls it held.
1299    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    /// A source whose registration fails, as inotify's does past the user's
1364    /// watch limit.
1365    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        // The stop joins the worker and never waits for the registration.
1425        assert_eq!(watch.stop_and_join(), SqliteWatchJoin::Joined);
1426        assert_eq!(watch.registration(), SqliteWatchRegistration::Pending);
1427        assert_eq!(held.live_sources(), 1);
1428
1429        // Once the platform call returns, the registration discards its source
1430        // on its own thread instead of handing it to the stopped watch.
1431        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        // Its thread stayed with this watch, which joins it now.
1438        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        // Dropping never waits for the registration and never detaches it:
1459        // the process keeps its thread, and reports it.
1460        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        // Once the platform call returns, the registration discards its
1467        // source, and the process joins its thread.
1468        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        // The stop cancels the held registration without ending it: it is
1497        // still pending, and dropping the watch hands exactly it to the
1498        // process.
1499        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        // Released before the watch starts: the registration completes at
1525        // once, without a platform call.
1526        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                &registration,
1559                SqliteWatchRegistration::Failed(reason)
1560                    if reason.contains("the watch limit is reached")
1561            ),
1562            "{registration:?}"
1563        );
1564    }
1565
1566    /// Set in the child process of the panicking-registration test.
1567    const PANIC_CHILD: &str = "MEERKAT_TEST_SQLITE_WATCH_PANIC_CHILD";
1568    /// What that child prints once its watch has survived.
1569    const PANIC_CHILD_DONE: &str = "SQLITE_WATCH_PANIC_CONTAINED";
1570
1571    /// A source whose registration panics and whose drop panics again, as
1572    /// notify 8.2.0's inotify watcher does when its event-loop thread never
1573    /// started: dropping it while the first panic unwinds aborts the process.
1574    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    /// The registration panics in a child process: a panic during a panic
1603    /// aborts the whole process, so only a child can show the watch survives.
1604    #[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)] // the test re-runs its own binary
1632        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    /// Set in the child process that runs one test of this module alone.
1652    const ALONE_CHILD: &str = "MEERKAT_TEST_SQLITE_WATCH_ALONE_CHILD";
1653    /// What that child prints once the test passed.
1654    const ALONE_CHILD_DONE: &str = "SQLITE_WATCH_ALONE_TEST_PASSED";
1655
1656    /// Run `body`, the body of this module's test `test`, alone in a child
1657    /// process of this test binary, and require it to pass there. The
1658    /// registrations in flight are counted across the process, so a test
1659    /// that takes every place shares its process with no other watch test,
1660    /// whatever runs the tests: nextest's process per test, or plain `cargo
1661    /// test`'s concurrent threads in one process.
1662    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)] // the test re-runs its own binary
1669        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                // Alone in its process: no other registration holds a place.
1694                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                // One more watch starts with no registration thread, on its sweep
1714                // alone.
1715                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                        &registration,
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                // Released, every held registration settles and gives its place back,
1742                // and a fresh watch registers again.
1743                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    /// A burst of 64 registrations held at once, as a parallel startup holds
1776    /// them while FSEvents stalls, all reach their platform call: none is
1777    /// refused at the cap and left on its sweep for life. The burst is
1778    /// written out, not taken from the cap, so a lower cap fails here.
1779    #[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                // Alone in its process: no other registration holds a place.
1786                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    /// Every WARN event this test process emitted, with the name of the
1823    /// thread that emitted it.
1824    static WARNINGS: Mutex<Vec<(Option<String>, String)>> = Mutex::new(Vec::new());
1825
1826    /// A process-wide subscriber that records WARN events into [`WARNINGS`]: a
1827    /// registration settles on its own thread, which a thread-local
1828    /// subscriber would never see.
1829    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    /// Install [`WarningCapture`] as this process's global subscriber, once.
1866    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    /// A source whose registration waits for its release, then fails, as an
1885    /// inotify watch past the user's limit does.
1886    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    /// Start a watch whose registration fails once released. Returns the
1924    /// watch, the signal that its registration entered, and its release.
1925    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        // A live watch whose registration fails reports it and warns once:
1950        // the capture sees registration threads.
1951        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        // A watch that stopped first is gone: its failed registration is
1965        // cancelled, with no warning about a sweep nobody runs any more.
1966        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}