Skip to main content

brows3r_lib/search/
cancel.rs

1//! Lightweight cancellation token for long-running search operations.
2//!
3//! `CancellationToken` is an `Arc<AtomicBool>` wrapper.  The `cancel()` call
4//! sets the flag; the background task polls `is_cancelled()` between pages and
5//! exits early when the flag is set.
6//!
7//! This deliberately avoids `tokio_util::sync::CancellationToken` to keep the
8//! dependency surface small — the atomics-based approach is sufficient for a
9//! single-flag stop signal.
10//!
11//! OCP: wrapping the `Arc<AtomicBool>` in this struct means the token shape
12//! can be extended (e.g. adding a reason code) without changing call sites.
13
14use std::sync::{
15    atomic::{AtomicBool, Ordering},
16    Arc,
17};
18
19// ---------------------------------------------------------------------------
20// CancellationToken
21// ---------------------------------------------------------------------------
22
23/// A cloneable, thread-safe cancellation flag.
24///
25/// Clone the token to share it between the producer (registry) and the
26/// consumer (background task).  Calling `cancel()` on any clone sets the flag
27/// for all clones.
28#[derive(Clone, Default)]
29pub struct CancellationToken {
30    atomic: Arc<AtomicBool>,
31}
32
33impl CancellationToken {
34    /// Create a new, uncancelled token.
35    pub fn new() -> Self {
36        Self {
37            atomic: Arc::new(AtomicBool::new(false)),
38        }
39    }
40
41    /// Signal cancellation.  Idempotent — safe to call multiple times.
42    pub fn cancel(&self) {
43        self.atomic.store(true, Ordering::Relaxed);
44    }
45
46    /// Returns `true` once `cancel()` has been called on any clone.
47    pub fn is_cancelled(&self) -> bool {
48        self.atomic.load(Ordering::Relaxed)
49    }
50}
51
52// ---------------------------------------------------------------------------
53// Tests
54// ---------------------------------------------------------------------------
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn new_token_is_not_cancelled() {
62        let token = CancellationToken::new();
63        assert!(!token.is_cancelled());
64    }
65
66    #[test]
67    fn cancel_sets_flag() {
68        let token = CancellationToken::new();
69        token.cancel();
70        assert!(token.is_cancelled());
71    }
72
73    #[test]
74    fn cancel_is_idempotent() {
75        let token = CancellationToken::new();
76        token.cancel();
77        token.cancel(); // second call must not panic
78        assert!(token.is_cancelled());
79    }
80
81    #[test]
82    fn clone_shares_flag() {
83        let token = CancellationToken::new();
84        let clone = token.clone();
85        token.cancel();
86        assert!(clone.is_cancelled(), "clone must see the cancellation");
87    }
88
89    #[test]
90    fn cancel_on_clone_visible_on_original() {
91        let token = CancellationToken::new();
92        let clone = token.clone();
93        clone.cancel();
94        assert!(
95            token.is_cancelled(),
96            "original must see clone's cancellation"
97        );
98    }
99}