Skip to main content

qubit_progress/
auto_reporter.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Scoped background reporting for one exclusively borrowed progress operation.
9// qubit-style: allow multiple-public-types
10
11use std::marker::PhantomData;
12use std::panic::AssertUnwindSafe;
13use std::panic::catch_unwind;
14use std::panic::resume_unwind;
15use std::sync::Arc;
16use std::sync::Weak;
17use std::sync::atomic::AtomicBool;
18use std::sync::atomic::Ordering;
19use std::sync::mpsc::Receiver;
20use std::sync::mpsc::SyncSender;
21use std::sync::mpsc::sync_channel;
22use std::thread;
23use std::thread::ScopedJoinHandle;
24
25use crate::AutoReporterError;
26use crate::EmissionError;
27use crate::Progress;
28use crate::WorkerPanic;
29
30/// Handle controlling one scoped automatic reporter.
31#[must_use]
32pub struct AutoReporter<'scope, 'reporter> {
33    /// Scoped worker result, present only for enabled operations.
34    join: Option<ScopedJoinHandle<'scope, Result<(), EmissionError>>>,
35    /// Shared wake and stop controls.
36    inner: Option<Arc<AutoReporterInner>>,
37    /// State observable by workers.
38    status: AutoReporterStatus,
39    /// Ties the original mutable Progress borrow to this handle's lifetime.
40    progress_borrow: PhantomData<&'scope mut Progress<'reporter>>,
41}
42
43impl<'scope, 'reporter> AutoReporter<'scope, 'reporter> {
44    /// Returns a cloneable worker notification handle.
45    #[must_use]
46    pub fn notifier(&self) -> ProgressNotifier {
47        ProgressNotifier {
48            inner: self.inner.as_ref().and_then(|inner| {
49                inner.notification_driven.then(|| Arc::downgrade(inner))
50            }),
51        }
52    }
53
54    /// Returns a cloneable status view for workers.
55    #[must_use]
56    pub fn status(&self) -> AutoReporterStatus {
57        self.status.clone()
58    }
59
60    /// Stops, joins and returns the background report result.
61    ///
62    /// A reporter or snapshot error is returned as
63    /// [`AutoReporterError::Emission`]. A worker panic is captured as
64    /// [`AutoReporterError::Panicked`] after the worker has been joined.
65    ///
66    /// # Errors
67    ///
68    /// Returns a reporter emission failure or a structured worker panic.
69    pub fn stop(mut self) -> Result<(), AutoReporterError> {
70        self.signal_stop();
71        match self.join_worker() {
72            Ok(result) => result.map_err(AutoReporterError::Emission),
73            Err(panic) => Err(AutoReporterError::Panicked(panic)),
74        }
75    }
76
77    /// Marks stop and wakes the reporter if it is blocked.
78    fn signal_stop(&self) {
79        if let Some(inner) = &self.inner {
80            inner.stopped.store(true, Ordering::Release);
81            wake(&inner.wake_sender);
82        }
83    }
84
85    /// Joins the scoped worker once and returns either its result or panic.
86    fn join_worker(
87        &mut self,
88    ) -> Result<Result<(), EmissionError>, WorkerPanic> {
89        let Some(join) = self.join.take() else {
90            return Ok(Ok(()));
91        };
92        join.join().map_err(WorkerPanic::new)
93    }
94}
95
96impl Drop for AutoReporter<'_, '_> {
97    /// Stops and joins a forgotten reporter without silently leaving a thread.
98    fn drop(&mut self) {
99        self.signal_stop();
100        match self.join_worker() {
101            Ok(Ok(())) => {}
102            Ok(Err(_)) => self.status.mark_failed(),
103            Err(_) => self.status.mark_failed(),
104        }
105    }
106}
107
108/// Notification handle that coalesces state changes without claiming delivery.
109#[derive(Clone)]
110pub struct ProgressNotifier {
111    /// Non-owning link present only for notification-driven reporters.
112    inner: Option<Weak<AutoReporterInner>>,
113}
114
115impl ProgressNotifier {
116    /// Records that shared work state changed and wakes a zero-interval loop.
117    ///
118    /// The method is a no-op for disabled and heartbeat-driven reporters, after
119    /// the reporter stops, and when no worker remains. Multiple calls merge
120    /// into at most one pending report.
121    pub fn notify(&self) {
122        let Some(inner) = self.inner.as_ref().and_then(Weak::upgrade) else {
123            return;
124        };
125        if inner.stopped.load(Ordering::Acquire) {
126            return;
127        }
128        inner.pending.store(true, Ordering::Release);
129        wake(&inner.wake_sender);
130    }
131}
132
133/// Cloneable status exposed while an automatic reporter is active.
134#[derive(Clone)]
135pub struct AutoReporterStatus {
136    /// Shared failure flag.
137    failed: Arc<AtomicBool>,
138}
139
140impl AutoReporterStatus {
141    /// Creates a status flag initially representing a healthy reporter.
142    fn healthy() -> Self {
143        Self {
144            failed: Arc::new(AtomicBool::new(false)),
145        }
146    }
147
148    /// Records that the reporter has failed.
149    fn mark_failed(&self) {
150        self.failed.store(true, Ordering::Release);
151    }
152
153    /// Returns whether the automatic reporter terminated with an error or
154    /// panic.
155    #[must_use]
156    pub fn is_failed(&self) -> bool {
157        self.failed.load(Ordering::Acquire)
158    }
159}
160
161/// Shared control state held by the handle and weakly by worker notifiers.
162struct AutoReporterInner {
163    /// Whether state-change notifications drive running reports.
164    notification_driven: bool,
165    /// Bounded wake channel sender.
166    wake_sender: SyncSender<()>,
167    /// Stop request flag.
168    stopped: AtomicBool,
169    /// Coalesced notification flag.
170    pending: AtomicBool,
171}
172
173/// Spawns the worker for one enabled progress operation.
174pub(crate) fn spawn<'scope, 'env, 'reporter>(
175    progress: &'scope mut Progress<'reporter>,
176    scope: &'scope thread::Scope<'scope, 'env>,
177) -> AutoReporter<'scope, 'reporter>
178where
179    'reporter: 'scope,
180{
181    let status = AutoReporterStatus::healthy();
182    if !progress.is_enabled() {
183        return AutoReporter {
184            join: None,
185            inner: None,
186            status,
187            progress_borrow: PhantomData,
188        };
189    }
190    let (wake_sender, wake_receiver) = sync_channel(1);
191    let inner = Arc::new(AutoReporterInner {
192        notification_driven: progress.report_interval().is_zero(),
193        wake_sender,
194        stopped: AtomicBool::new(false),
195        pending: AtomicBool::new(false),
196    });
197    let worker_inner = Arc::clone(&inner);
198    let worker_status = status.clone();
199    let join = scope.spawn(move || {
200        match catch_unwind(AssertUnwindSafe(|| {
201            run(progress, Arc::clone(&worker_inner), wake_receiver)
202        })) {
203            Ok(result) => {
204                if result.is_err() {
205                    worker_status.mark_failed();
206                    worker_inner.stopped.store(true, Ordering::Release);
207                }
208                result
209            }
210            Err(payload) => {
211                worker_status.mark_failed();
212                worker_inner.stopped.store(true, Ordering::Release);
213                resume_unwind(payload)
214            }
215        }
216    });
217    AutoReporter {
218        join: Some(join),
219        inner: Some(inner),
220        status,
221        progress_borrow: PhantomData,
222    }
223}
224
225/// Runs one background reporting loop until stopped or a report fails.
226fn run(
227    progress: &mut Progress<'_>,
228    inner: Arc<AutoReporterInner>,
229    receiver: Receiver<()>,
230) -> Result<(), EmissionError> {
231    if progress.report_interval().is_zero() {
232        run_notified(progress, &inner, receiver)
233    } else {
234        run_heartbeat(progress, &inner, receiver)
235    }
236}
237
238/// Runs notification-driven reporting for a zero interval.
239fn run_notified(
240    progress: &mut Progress<'_>,
241    inner: &AutoReporterInner,
242    receiver: Receiver<()>,
243) -> Result<(), EmissionError> {
244    loop {
245        receiver
246            .recv()
247            .expect("notification sender must outlive the reporter worker");
248        if inner.pending.swap(false, Ordering::AcqRel) {
249            progress.report()?;
250        }
251        if inner.stopped.load(Ordering::Acquire) {
252            return Ok(());
253        }
254    }
255}
256
257/// Runs deadline-based heartbeat reporting for a positive interval.
258fn run_heartbeat(
259    progress: &mut Progress<'_>,
260    inner: &AutoReporterInner,
261    receiver: Receiver<()>,
262) -> Result<(), EmissionError> {
263    loop {
264        if inner.stopped.load(Ordering::Acquire) {
265            return Ok(());
266        }
267        let timeout = progress.time_until_due();
268        if receiver.recv_timeout(timeout).is_ok() {
269            return Ok(());
270        }
271        progress.report_if_due()?;
272    }
273}
274
275/// Sends one coalesced wake signal without blocking a worker.
276fn wake(sender: &SyncSender<()>) {
277    let _ = sender.try_send(());
278}