Skip to main content

nodedb_lite/
tasks.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Lifecycle for the database's background tasks.
4//!
5//! Auto-flush, auto-compact and the sync loop all outlive the call that starts
6//! them. Left detached, nothing can stop them: the host tears its async runtime
7//! down while a task is still polling, and on a native runtime that lands the
8//! process somewhere it cannot recover from.
9//!
10//! Every such task is registered here with a stop signal and a handle. Shutdown
11//! is cooperative first: the signal is set, each task leaves its loop at a point
12//! it chose, and only a task that ignores the signal past
13//! [`TASK_STOP_TIMEOUT`] is aborted. Cancelling a database task at an arbitrary
14//! await point can drop it mid-write, so abort is the backstop, never the
15//! mechanism.
16
17use std::sync::Mutex;
18use std::time::Duration;
19
20use tokio::sync::watch;
21
22use crate::runtime::{Interval, TaskHandle};
23
24/// How long shutdown waits for a task to leave its loop before aborting it.
25pub const TASK_STOP_TIMEOUT: Duration = Duration::from_secs(5);
26
27/// Which background task a registration belongs to.
28///
29/// Lets [`TaskRegistry::stop`] cancel one kind — stopping sync without closing
30/// the database — while shutdown stops every kind.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum TaskKind {
33    AutoFlush,
34    AutoCompact,
35    Sync,
36}
37
38/// The stop side of a task's signal, held by the task itself.
39pub struct StopSignal {
40    rx: watch::Receiver<bool>,
41}
42
43impl StopSignal {
44    /// Whether shutdown has been requested.
45    pub fn is_stopping(&self) -> bool {
46        *self.rx.borrow()
47    }
48
49    /// Resolve once shutdown is requested.
50    ///
51    /// Resolves immediately when the signal is already set, and also when the
52    /// sender is gone — a dropped registry means the database is going away,
53    /// which is the same answer.
54    pub async fn stopped(&mut self) {
55        while !*self.rx.borrow_and_update() {
56            if self.rx.changed().await.is_err() {
57                return;
58            }
59        }
60    }
61
62    /// Wait for the next tick of `ticker`, unless shutdown comes first.
63    ///
64    /// Returns `false` when the task must leave its loop. A periodic task
65    /// polls on a long period — minutes, for compaction — so waiting for the
66    /// next tick to notice the signal would stall shutdown for that long.
67    pub async fn tick_or_stop(&mut self, ticker: &mut Interval) -> bool {
68        tokio::select! {
69            _ = ticker.tick() => !self.is_stopping(),
70            _ = self.stopped() => false,
71        }
72    }
73}
74
75/// One registered task: its stop signal and its handle.
76struct Registered {
77    kind: TaskKind,
78    stop: watch::Sender<bool>,
79    handle: TaskHandle,
80}
81
82/// The database's set of running background tasks.
83#[derive(Default)]
84pub struct TaskRegistry {
85    tasks: Mutex<Vec<Registered>>,
86}
87
88impl TaskRegistry {
89    /// Create the stop signal for a task that is about to be spawned.
90    ///
91    /// The sender is handed back to [`TaskRegistry::track`] with the task's
92    /// handle once the spawn succeeds. Separating the two keeps the registry
93    /// free of the `Send` bound that differs between native and WASM spawns.
94    pub fn signal() -> (watch::Sender<bool>, StopSignal) {
95        let (tx, rx) = watch::channel(false);
96        (tx, StopSignal { rx })
97    }
98
99    /// Record a spawned task so shutdown can stop it.
100    pub fn track(&self, kind: TaskKind, stop: watch::Sender<bool>, handle: TaskHandle) {
101        self.lock().push(Registered { kind, stop, handle });
102    }
103
104    /// Whether a task of `kind` is registered.
105    pub fn has(&self, kind: TaskKind) -> bool {
106        self.lock().iter().any(|task| task.kind == kind)
107    }
108
109    /// Stop every task of `kind` and wait for it to wind down.
110    ///
111    /// Returns `true` when at least one task was running.
112    pub async fn stop(&self, kind: TaskKind) -> bool {
113        let taken: Vec<Registered> = {
114            let mut tasks = self.lock();
115            let (matching, rest) = tasks.drain(..).partition(|task| task.kind == kind);
116            *tasks = rest;
117            matching
118        };
119        let stopped = !taken.is_empty();
120        Self::wind_down(taken).await;
121        stopped
122    }
123
124    /// Stop every registered task and wait for them to wind down.
125    ///
126    /// Idempotent: a second call has nothing left to stop.
127    pub async fn shutdown(&self) {
128        let taken: Vec<Registered> = self.lock().drain(..).collect();
129        Self::wind_down(taken).await;
130    }
131
132    /// Signal each task, then join it, aborting whatever outlasts the timeout.
133    async fn wind_down(tasks: Vec<Registered>) {
134        // Signal every task before joining any of them, so their wind-downs
135        // overlap instead of costing the timeout each in turn.
136        for task in &tasks {
137            let _ = task.stop.send(true);
138        }
139        for mut task in tasks {
140            if !task.handle.join_within(TASK_STOP_TIMEOUT).await {
141                tracing::warn!(
142                    kind = ?task.kind,
143                    timeout_ms = TASK_STOP_TIMEOUT.as_millis(),
144                    "background task ignored its stop signal; aborting"
145                );
146                task.handle.abort();
147            }
148        }
149    }
150
151    /// Take the lock, recovering from a poisoned one.
152    ///
153    /// A panic in a task's registration must not make the database
154    /// unclosable — the whole point of the registry is that shutdown works.
155    fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Registered>> {
156        self.tasks.lock().unwrap_or_else(|e| e.into_inner())
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[tokio::test]
165    async fn stop_signal_resolves_when_set() {
166        let (tx, mut signal) = TaskRegistry::signal();
167        assert!(!signal.is_stopping());
168        tx.send(true).unwrap();
169        signal.stopped().await;
170        assert!(signal.is_stopping());
171    }
172
173    #[tokio::test]
174    async fn stop_signal_resolves_when_sender_dropped() {
175        let (tx, mut signal) = TaskRegistry::signal();
176        drop(tx);
177        // A dropped registry means the database is going away.
178        signal.stopped().await;
179    }
180
181    #[tokio::test]
182    async fn shutdown_stops_a_registered_task() {
183        let registry = TaskRegistry::default();
184        let (tx, mut signal) = TaskRegistry::signal();
185        let handle = crate::runtime::spawn(async move {
186            signal.stopped().await;
187        });
188        registry.track(TaskKind::AutoFlush, tx, handle);
189        assert!(registry.has(TaskKind::AutoFlush));
190
191        registry.shutdown().await;
192        assert!(!registry.has(TaskKind::AutoFlush));
193    }
194
195    #[tokio::test]
196    async fn stop_takes_only_the_named_kind() {
197        let registry = TaskRegistry::default();
198        for kind in [TaskKind::AutoFlush, TaskKind::Sync] {
199            let (tx, mut signal) = TaskRegistry::signal();
200            let handle = crate::runtime::spawn(async move {
201                signal.stopped().await;
202            });
203            registry.track(kind, tx, handle);
204        }
205
206        assert!(registry.stop(TaskKind::Sync).await);
207        assert!(!registry.has(TaskKind::Sync));
208        assert!(registry.has(TaskKind::AutoFlush));
209
210        // Nothing of that kind is left to stop.
211        assert!(!registry.stop(TaskKind::Sync).await);
212
213        registry.shutdown().await;
214    }
215
216    #[tokio::test]
217    async fn shutdown_aborts_a_task_that_ignores_the_signal() {
218        let registry = TaskRegistry::default();
219        let (tx, _signal) = TaskRegistry::signal();
220        let handle = crate::runtime::spawn(async move {
221            // Never observes the signal; shutdown must abort it.
222            std::future::pending::<()>().await;
223        });
224        registry.track(TaskKind::Sync, tx, handle);
225
226        // Would hang without the abort backstop; the timeout bounds it.
227        tokio::time::timeout(TASK_STOP_TIMEOUT * 2, registry.shutdown())
228            .await
229            .expect("shutdown must not outlast the stop timeout");
230    }
231}