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}