1use std::cell::RefCell;
23use std::sync::Arc;
24use std::sync::atomic::{AtomicBool, Ordering};
25
26use crate::{OgeomError, OgeomResult};
27
28#[derive(Debug, Clone, Copy)]
31pub struct Stage<'a> {
32 pub name: &'a str,
35 pub progress: Option<(u64, u64)>,
38}
39
40type Sink = Arc<dyn Fn(Stage<'_>) + Send + Sync>;
42
43#[derive(Clone)]
45struct State {
46 cancel: Arc<AtomicBool>,
47 sink: Option<Sink>,
48}
49
50impl core::fmt::Debug for State {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 f.debug_struct("State")
53 .field("cancelled", &self.cancel.load(Ordering::Relaxed))
54 .field("has_sink", &self.sink.is_some())
55 .finish()
56 }
57}
58
59thread_local! {
60 static ACTIVE: RefCell<Option<State>> = const { RefCell::new(None) };
61}
62
63#[derive(Debug)]
66pub struct Watch {
67 state: State,
68}
69
70impl Watch {
71 #[must_use]
73 pub fn new() -> Self {
74 Self {
75 state: State {
76 cancel: Arc::new(AtomicBool::new(false)),
77 sink: None,
78 },
79 }
80 }
81
82 #[must_use]
88 pub fn with_sink(sink: impl Fn(&str) + Send + Sync + 'static) -> Self {
89 Self::with_stage_sink(move |stage: Stage<'_>| sink(stage.name))
90 }
91
92 #[must_use]
97 pub fn with_stage_sink(sink: impl Fn(Stage<'_>) + Send + Sync + 'static) -> Self {
98 Self {
99 state: State {
100 cancel: Arc::new(AtomicBool::new(false)),
101 sink: Some(Arc::new(sink)),
102 },
103 }
104 }
105
106 #[must_use]
109 pub fn canceller(&self) -> Canceller {
110 Canceller {
111 cancel: Arc::clone(&self.state.cancel),
112 }
113 }
114}
115
116impl Default for Watch {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct Canceller {
125 cancel: Arc<AtomicBool>,
126}
127
128impl Canceller {
129 pub fn cancel(&self) {
132 self.cancel.store(true, Ordering::SeqCst);
133 }
134
135 #[must_use]
137 pub fn is_cancelled(&self) -> bool {
138 self.cancel.load(Ordering::SeqCst)
139 }
140}
141
142struct Scope {
145 previous: Option<State>,
146}
147
148impl Drop for Scope {
149 fn drop(&mut self) {
150 ACTIVE.with(|active| {
151 *active.borrow_mut() = self.previous.take();
152 });
153 }
154}
155
156pub fn watched<T>(watch: &Watch, f: impl FnOnce() -> T) -> T {
159 let previous = ACTIVE.with(|active| active.borrow_mut().replace(watch.state.clone()));
160 let _scope = Scope { previous };
161 f()
162}
163
164pub fn checkpoint() -> OgeomResult<()> {
172 ACTIVE.with(|active| {
173 if let Some(state) = active.borrow().as_ref()
174 && state.cancel.load(Ordering::Relaxed)
175 {
176 return Err(OgeomError::Cancelled);
177 }
178 Ok(())
179 })
180}
181
182pub fn stage(name: &str) {
184 announce(Stage {
185 name,
186 progress: None,
187 });
188}
189
190pub fn stage_at(name: &str, done: u64, total: u64) {
195 announce(Stage {
196 name,
197 progress: Some((done, total)),
198 });
199}
200
201fn announce(stage: Stage<'_>) {
202 ACTIVE.with(|active| {
203 if let Some(state) = active.borrow().as_ref()
204 && let Some(sink) = &state.sink
205 {
206 sink(stage);
207 }
208 });
209}
210
211#[must_use]
213pub fn snapshot() -> Option<WatchSnapshot> {
214 ACTIVE.with(|active| active.borrow().clone().map(|state| WatchSnapshot { state }))
215}
216
217pub fn with_snapshot<T>(snapshot: Option<&WatchSnapshot>, f: impl FnOnce() -> T) -> T {
220 match snapshot {
221 Some(snap) => {
222 let watch = Watch {
223 state: snap.state.clone(),
224 };
225 watched(&watch, f)
226 }
227 None => f(),
228 }
229}
230
231#[derive(Debug, Clone)]
233pub struct WatchSnapshot {
234 state: State,
235}
236
237#[cfg(test)]
238#[allow(clippy::unwrap_used)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn counts_reach_a_stage_sink_and_names_still_reach_a_plain_one() {
244 use std::sync::Mutex;
245 type Heard = Vec<(String, Option<(u64, u64)>)>;
246 let heard: Arc<Mutex<Heard>> = Arc::new(Mutex::new(Vec::new()));
247 let record = Arc::clone(&heard);
248 let watch = Watch::with_stage_sink(move |stage: Stage<'_>| {
249 record
250 .lock()
251 .unwrap()
252 .push((stage.name.to_owned(), stage.progress));
253 });
254 watched(&watch, || {
255 stage("plain");
256 stage_at("counted", 2, 5);
257 });
258 assert_eq!(
259 *heard.lock().unwrap(),
260 vec![
261 ("plain".to_owned(), None),
262 ("counted".to_owned(), Some((2, 5))),
263 ]
264 );
265
266 let names: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
268 let record = Arc::clone(&names);
269 let watch =
270 Watch::with_sink(move |name: &str| record.lock().unwrap().push(name.to_owned()));
271 watched(&watch, || {
272 stage_at("counted", 1, 3);
273 });
274 assert_eq!(*names.lock().unwrap(), vec!["counted".to_owned()]);
275 }
276
277 #[test]
278 fn unwatched_checkpoints_are_free_and_fine() {
279 assert!(checkpoint().is_ok());
280 stage("nothing listens");
281 }
282
283 #[test]
284 fn a_cancelled_watch_stops_the_next_checkpoint() {
285 let watch = Watch::new();
286 let stop = watch.canceller();
287 let result: OgeomResult<()> = watched(&watch, || {
288 checkpoint()?;
289 stop.cancel();
290 checkpoint()?;
291 unreachable!("the second checkpoint must refuse");
292 });
293 assert!(matches!(result, Err(OgeomError::Cancelled)));
294 assert!(stop.is_cancelled());
295 assert!(checkpoint().is_ok());
297 }
298
299 #[test]
300 fn stages_reach_the_sink_and_scopes_nest() {
301 use std::sync::Mutex;
302 let heard: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
303 let record = Arc::clone(&heard);
304 let outer = Watch::with_sink(move |name| record.lock().unwrap().push(name.to_owned()));
305 let inner = Watch::new();
306 watched(&outer, || {
307 stage("first");
308 watched(&inner, || {
309 stage("masked");
311 });
312 stage("second");
313 });
314 assert_eq!(*heard.lock().unwrap(), ["first", "second"]);
315 }
316
317 #[test]
318 fn snapshots_carry_the_watch_across_threads() {
319 let watch = Watch::new();
320 watch.canceller().cancel();
321 let snap = watched(&watch, snapshot);
322 let outcome: OgeomResult<()> =
323 std::thread::spawn(move || with_snapshot(snap.as_ref(), checkpoint))
324 .join()
325 .unwrap();
326 assert!(matches!(outcome, Err(OgeomError::Cancelled)));
327 }
328}