Skip to main content

zipatch_rs/apply/
cancel.rs

1//! Standalone cancellation primitive for apply-time aborts.
2//!
3//! [`CancelToken`] is a cheap, cloneable handle wrapping an
4//! [`AtomicBool`](std::sync::atomic::AtomicBool). One handle is held by the
5//! thread driving the apply ([`ApplyConfig::apply_patch`](crate::ApplyConfig::apply_patch)
6//! or [`IndexApplier::execute`](crate::IndexApplier::execute)) and one or
7//! more clones live wherever cancellation is initiated — typically a UI
8//! event handler or a `tokio::select!` arm.
9//!
10//! # Why a separate primitive
11//!
12//! Cancellation can also be signalled by an [`ApplyObserver`](crate::ApplyObserver)
13//! that returns `true` from [`ApplyObserver::should_cancel`](crate::ApplyObserver::should_cancel),
14//! but that route forces a consumer who only wants cancellation to implement
15//! the entire trait. The token makes the common case trivial:
16//!
17//! ```no_run
18//! use zipatch_rs::{ApplyConfig, CancelToken, open_patch};
19//!
20//! let token = CancelToken::new();
21//! let bg = token.clone();
22//! std::thread::spawn(move || {
23//!     // Some other thread flips the flag on a user gesture.
24//!     bg.cancel();
25//! });
26//!
27//! let reader = open_patch("patch.patch").unwrap();
28//! let _ = ApplyConfig::new("/opt/ffxiv/game")
29//!     .with_cancel_token(token)
30//!     .apply_patch(reader);
31//! ```
32//!
33//! When both a token and an observer are installed, the apply driver polls
34//! both — either source can request cancellation independently, and they
35//! compose without a wrapper observer.
36
37use std::sync::Arc;
38use std::sync::atomic::{AtomicBool, Ordering};
39
40/// Cloneable cancellation flag shared between the apply worker and any
41/// number of signalling threads.
42///
43/// `Clone` is `O(1)` — every handle points at the same underlying
44/// [`AtomicBool`]. Marking the token cancelled from any handle is visible to
45/// every other handle on the next [`Self::is_cancelled`] poll.
46///
47/// # Threading
48///
49/// `Send + Sync` is automatic: the inner [`Arc`] of [`AtomicBool`] is
50/// trivially shareable across threads. Construct on one thread, clone, and
51/// hand the clones to wherever they need to live.
52///
53/// # Cost
54///
55/// [`Self::is_cancelled`] is a single [`Ordering::Relaxed`] atomic load and
56/// is cheap enough to call from the inner SQPK block loop. [`Self::cancel`]
57/// is one relaxed store. The token allocates one [`Arc`]-managed
58/// [`AtomicBool`] at construction; clones share it.
59#[derive(Clone, Debug, Default)]
60pub struct CancelToken(Arc<AtomicBool>);
61
62impl CancelToken {
63    /// Construct a fresh, un-cancelled token.
64    #[must_use]
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    /// Request cancellation.
70    ///
71    /// Idempotent — calling this on an already-cancelled token is a no-op.
72    /// The flip is immediately visible to every clone of this token.
73    pub fn cancel(&self) {
74        self.0.store(true, Ordering::Relaxed);
75    }
76
77    /// Returns `true` if [`Self::cancel`] has been called on any clone of
78    /// this token.
79    ///
80    /// Uses [`Ordering::Relaxed`] — the apply driver polls this in hot
81    /// loops and only needs eventual visibility, not synchronisation with
82    /// any other memory ops.
83    #[must_use]
84    pub fn is_cancelled(&self) -> bool {
85        self.0.load(Ordering::Relaxed)
86    }
87
88    /// Clear the cancellation flag.
89    ///
90    /// Useful for retry-after-cancel scenarios where the consumer wants to
91    /// reuse the same token (and therefore avoid re-wiring its clones)
92    /// across multiple apply attempts. The flip is immediately visible to
93    /// every clone.
94    pub fn reset(&self) {
95        self.0.store(false, Ordering::Relaxed);
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn new_token_is_not_cancelled() {
105        let t = CancelToken::new();
106        assert!(!t.is_cancelled());
107    }
108
109    #[test]
110    fn cancel_flips_the_flag() {
111        let t = CancelToken::new();
112        t.cancel();
113        assert!(t.is_cancelled());
114    }
115
116    #[test]
117    fn cancel_is_visible_across_clones() {
118        let a = CancelToken::new();
119        let b = a.clone();
120        assert!(!b.is_cancelled());
121        a.cancel();
122        assert!(
123            b.is_cancelled(),
124            "cancel on one handle must surface on the clone"
125        );
126    }
127
128    #[test]
129    fn cancel_is_idempotent() {
130        let t = CancelToken::new();
131        t.cancel();
132        t.cancel();
133        assert!(t.is_cancelled());
134    }
135
136    #[test]
137    fn reset_clears_the_flag() {
138        let t = CancelToken::new();
139        t.cancel();
140        assert!(t.is_cancelled());
141        t.reset();
142        assert!(!t.is_cancelled());
143    }
144
145    #[test]
146    fn reset_is_visible_across_clones() {
147        let a = CancelToken::new();
148        let b = a.clone();
149        a.cancel();
150        assert!(b.is_cancelled());
151        b.reset();
152        assert!(
153            !a.is_cancelled(),
154            "reset on one handle must surface on the clone"
155        );
156    }
157
158    #[test]
159    fn cancel_propagates_between_threads() {
160        let t = CancelToken::new();
161        let bg = t.clone();
162        let handle = std::thread::spawn(move || {
163            bg.cancel();
164        });
165        handle.join().unwrap();
166        assert!(t.is_cancelled());
167    }
168
169    #[test]
170    fn token_is_send_and_sync() {
171        fn assert_send_sync<T: Send + Sync>() {}
172        assert_send_sync::<CancelToken>();
173    }
174}