cancel/
lib.rs

1//! This crate provides a `Token` that can be used to co-operatively
2//! signal when an operation should be canceled.
3//!
4//! ```rust
5//! use cancel::{Canceled, Token};
6//! use std::time::Duration;
7//!
8//! fn do_something(token: &Token) -> Result<bool, Canceled> {
9//!   loop {
10//!     token.check_cancel()?;
11//!
12//!     // process more stuff here
13//!   }
14//!
15//!   Ok(true)
16//! }
17//!
18//! fn start_something() -> Result<bool, Canceled> {
19//!   let token = Token::with_duration(Duration::new(10, 0));
20//!   do_something(&token)
21//! }
22//! ```
23
24use std::sync::atomic::{AtomicBool, Ordering};
25use std::time::{Duration, Instant};
26
27/// The Err value returned from `Token::check_cancel`.
28/// It indicates that the `Token` was canceled and that the operation
29/// should cease.
30#[derive(Debug)]
31pub struct Canceled {}
32
33impl std::error::Error for Canceled {}
34impl std::fmt::Display for Canceled {
35    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
36        write!(f, "Operation was Canceled")
37    }
38}
39
40/// A cancellation token.
41/// It tracks the state and holds an optional deadline for the operation.
42/// To share `Token` across threads, wrap it in a `std::sync::Arc`.
43#[derive(Debug, Default)]
44pub struct Token {
45    canceled: AtomicBool,
46    deadline: Option<Instant>,
47}
48
49impl Token {
50    /// Create a new Token with no deadline.  The token
51    /// will be marked as canceled only once `Token::cancel`
52    /// has been called.
53    pub fn new() -> Self {
54        Default::default()
55    }
56
57    /// Create a new Token with a deadline set to the current
58    /// clock plus the specified duration.  The token will be
59    /// marked as canceled either when `Token::cancel` is
60    /// called, or when the operation calls either `Token::is_canceled`
61    /// or `Token::check_cancel` and the current clock exceeds
62    /// the computed deadline.
63    pub fn with_duration(duration: Duration) -> Self {
64        Self {
65            canceled: AtomicBool::new(false),
66            deadline: Some(Instant::now() + duration),
67        }
68    }
69
70    /// Create a new Token with a deadline set to the specified
71    /// instant.  The token will be marked as canceled either when
72    /// `Token::cancel` is called, or when the operation calls
73    /// either `Token::is_canceled` or `Token::check_cancel` and
74    /// the current clock exceeds the specified deadline.
75    pub fn with_deadline(deadline: Instant) -> Self {
76        Self {
77            canceled: AtomicBool::new(false),
78            deadline: Some(deadline),
79        }
80    }
81
82    /// Explicitly mark the token as being canceled.
83    /// This method is async signal safe.
84    pub fn cancel(&self) {
85        self.canceled.store(true, Ordering::Release);
86    }
87
88    /// Check whether the token was canceled.
89    /// This method is intended to be called by code that initiated
90    /// (rather than performed) an operation to test whether that
91    /// operation was successful.
92    /// If you want to test for cancellation in the body of your
93    /// processing code you should use either `Token::is_canceled`
94    /// or `Token::check_cancel`.
95    /// Using `Token::check_cancel` to propagate a `Result` value
96    /// is often a cleaner design than using `Token::was_canceled`.
97    pub fn was_canceled(&self) -> bool {
98        self.canceled.load(Ordering::Acquire)
99    }
100
101    /// Test whether an ongoing operation should cease
102    /// due to cancellation.
103    /// If a deadline has been set, the current clock will be evaluated
104    /// and compared against the deadline, setting the state to canceled
105    /// if appropriate.
106    /// Returns true if the operation has been canceled.
107    pub fn is_canceled(&self) -> bool {
108        if self.was_canceled() {
109            true
110        } else if let Some(deadline) = self.deadline.as_ref() {
111            if Instant::now() > *deadline {
112                self.cancel();
113                true
114            } else {
115                false
116            }
117        } else {
118            false
119        }
120    }
121
122    /// Test whether an ongoing operation should cease
123    /// due to cancellation, propagating a `Canceled` error value
124    /// if the operation has been canceled.
125    /// If a deadline has been set, the current clock will be evaluated
126    /// and compared against the deadline, setting the state to canceled
127    /// if appropriate.
128    pub fn check_cancel(&self) -> Result<(), Canceled> {
129        if self.is_canceled() {
130            Err(Canceled {})
131        } else {
132            Ok(())
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use failure::Fallible;
141    use std::sync::Arc;
142
143    #[test]
144    fn it_works() {
145        let token = Token::new();
146        assert!(!token.was_canceled());
147        token.cancel();
148        assert!(token.was_canceled());
149    }
150
151    // Ensure that we work with the failure crate, but don't force our
152    // users to require the failure crate
153    fn check(token: &Token) -> Fallible<()> {
154        token.check_cancel()?;
155        Ok(())
156    }
157
158    #[test]
159    fn err() {
160        let token = Token::new();
161        token.cancel();
162        assert_eq!(true, token.check_cancel().is_err());
163        assert_eq!(true, check(&token).is_err());
164    }
165
166    #[test]
167    fn deadline() {
168        let hard_deadline = Instant::now() + Duration::new(2, 0);
169        let token = Token::with_duration(Duration::new(1, 0));
170        loop {
171            if token.is_canceled() {
172                break;
173            }
174
175            assert!(Instant::now() < hard_deadline);
176            std::thread::sleep(Duration::from_millis(200));
177        }
178    }
179
180    #[test]
181    fn threads() {
182        let token = Arc::new(Token::with_duration(Duration::new(1, 0)));
183        let shared = Arc::clone(&token);
184        let thr = std::thread::spawn(move || {
185            while !shared.is_canceled() {
186                std::thread::sleep(Duration::from_millis(200));
187            }
188            true
189        });
190        assert_eq!(true, thr.join().unwrap());
191        assert_eq!(true, token.was_canceled());
192    }
193}