1use std::sync::Mutex;
18use std::time::Duration;
19
20use tokio::sync::watch;
21
22use crate::runtime::{Interval, TaskHandle};
23
24pub const TASK_STOP_TIMEOUT: Duration = Duration::from_secs(5);
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum TaskKind {
33 AutoFlush,
34 AutoCompact,
35 Sync,
36}
37
38pub struct StopSignal {
40 rx: watch::Receiver<bool>,
41}
42
43impl StopSignal {
44 pub fn is_stopping(&self) -> bool {
46 *self.rx.borrow()
47 }
48
49 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 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
75struct Registered {
77 kind: TaskKind,
78 stop: watch::Sender<bool>,
79 handle: TaskHandle,
80}
81
82#[derive(Default)]
84pub struct TaskRegistry {
85 tasks: Mutex<Vec<Registered>>,
86}
87
88impl TaskRegistry {
89 pub fn signal() -> (watch::Sender<bool>, StopSignal) {
95 let (tx, rx) = watch::channel(false);
96 (tx, StopSignal { rx })
97 }
98
99 pub fn track(&self, kind: TaskKind, stop: watch::Sender<bool>, handle: TaskHandle) {
101 self.lock().push(Registered { kind, stop, handle });
102 }
103
104 pub fn has(&self, kind: TaskKind) -> bool {
106 self.lock().iter().any(|task| task.kind == kind)
107 }
108
109 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 pub async fn shutdown(&self) {
128 let taken: Vec<Registered> = self.lock().drain(..).collect();
129 Self::wind_down(taken).await;
130 }
131
132 async fn wind_down(tasks: Vec<Registered>) {
134 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 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 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 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 std::future::pending::<()>().await;
223 });
224 registry.track(TaskKind::Sync, tx, handle);
225
226 tokio::time::timeout(TASK_STOP_TIMEOUT * 2, registry.shutdown())
228 .await
229 .expect("shutdown must not outlast the stop timeout");
230 }
231}