use std::{
collections::HashMap,
sync::{Mutex, OnceLock},
};
use tokio_util::sync::CancellationToken;
static REGISTRY: OnceLock<Mutex<Registry>> = OnceLock::new();
#[derive(Default)]
struct Registry {
next_id: u64,
entries: HashMap<u64, (String, CancellationToken)>,
}
fn registry() -> &'static Mutex<Registry> {
REGISTRY.get_or_init(|| Mutex::new(Registry::default()))
}
#[must_use = "guard 必须存活到后台工作结束"]
pub struct BackgroundCancelGuard {
id: u64,
}
impl Drop for BackgroundCancelGuard {
fn drop(&mut self) {
registry()
.lock()
.unwrap_or_else(|e| e.into_inner())
.entries
.remove(&self.id);
}
}
pub fn register_background_cancel(
label: impl Into<String>,
token: CancellationToken,
) -> BackgroundCancelGuard {
let mut table = registry().lock().unwrap_or_else(|e| e.into_inner());
let id = table.next_id;
table.next_id += 1;
table.entries.insert(id, (label.into(), token));
BackgroundCancelGuard { id }
}
pub fn cancel_background_work() -> usize {
let table = registry().lock().unwrap_or_else(|e| e.into_inner());
for (_, token) in table.entries.values() {
token.cancel();
}
table.entries.len()
}
pub fn background_work_count() -> usize {
registry()
.lock()
.unwrap_or_else(|e| e.into_inner())
.entries
.len()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn register_cancel_and_unregister() {
let _g = crate::test_support::AUTH_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let before = background_work_count();
let token = CancellationToken::new();
let guard = register_background_cancel("test-job", token.clone());
assert_eq!(background_work_count(), before + 1);
assert!(!token.is_cancelled());
assert_eq!(cancel_background_work(), before + 1);
assert!(token.is_cancelled(), "取消后 token 必须已取消");
drop(guard);
assert_eq!(background_work_count(), before, "guard drop 后应摘除");
}
#[test]
fn cancelling_after_drop_is_a_no_op() {
let _g = crate::test_support::AUTH_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let before = background_work_count();
let done = register_background_cancel("done-job", CancellationToken::new());
drop(done);
let live_token = CancellationToken::new();
let live = register_background_cancel("live-job", live_token.clone());
let _ = cancel_background_work();
assert!(live_token.is_cancelled());
drop(live);
assert_eq!(background_work_count(), before);
}
}