model: a cancel flag on Progress and progress::check

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 10:39:40 -04:00
1 parent 3363181a01
commit a2d646a75e
2 files changed
+48

No files matched your search

+5
View File
@@ -110,6 +110,10 @@ pub enum Error {
/// A definition cannot be deleted while another refers to it.
#[error("in use: {0}")]
InUse(String),
/// The command was cancelled while it ran ([`progress::Progress::cancel`]); it left
/// nothing behind.
#[error("cancelled")]
Cancelled,
}
pub type Result<T> = std::result::Result<T, Error>;
@@ -366,6 +370,7 @@ pub(crate) fn context(what: &str, name: &str, e: Error) -> Error {
Error::NotFound(m) => Error::NotFound(format!("{what} `{shown}`: {m}")),
Error::Limit(m) => Error::Limit(format!("{what} `{shown}`: {m}")),
Error::InUse(m) => Error::InUse(format!("{what} `{shown}`: {m}")),
Error::Cancelled => Error::Cancelled,
}
}
+43
View File
@@ -9,8 +9,15 @@
//!
//! The reporter is per thread: work handed to a thread pool installs the caller's
//! ([`current`]) inside it if it reports from there. Reporting never changes a result.
//!
//! The same [`Progress`] carries the host's request to stop ([`Progress::cancel`]): the long
//! loops of the solvers and meshers ask [`check`] at their natural points (an increment, a time
//! step, a refinement round, a supernode) and return [`crate::Error::Cancelled`] once it is
//! set. Cancellation is cooperative: the work stops at the next such point, and the caller
//! discards what it was building, so a cancelled command changes nothing.
use std::cell::RefCell;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use serde::Serialize;
@@ -76,6 +83,7 @@ fn number(x: f64) -> String {
#[derive(Debug, Default)]
pub struct Progress {
state: Mutex<State>,
cancelled: AtomicBool,
}
impl Progress {
@@ -88,6 +96,22 @@ impl Progress {
self.state.lock().map(|s| s.clone()).unwrap_or_default()
}
/// Ask the command to stop at its next check ([`check`]); from any thread.
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Relaxed);
}
/// Whether [`Progress::cancel`] was called.
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
/// `Err(Cancelled)` once cancelled: for work on a thread that does not report (a thread
/// pool's), which holds the caller's [`Progress`] from [`current`].
pub fn check(&self) -> crate::Result<()> {
if self.is_cancelled() { Err(crate::Error::Cancelled) } else { Ok(()) }
}
fn update(&self, f: impl FnOnce(&mut State)) {
if let Ok(mut s) = self.state.lock() {
f(&mut s);
@@ -150,6 +174,12 @@ pub fn count(done: f64) {
with(|p| p.update(|s| s.done = done));
}
/// `Err(Cancelled)` when this thread's command was cancelled; `Ok` with no [`Progress`]
/// installed.
pub fn check() -> crate::Result<()> {
CURRENT.with(|c| c.borrow().as_ref().map_or(Ok(()), |p| p.check()))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -171,4 +201,17 @@ mod tests {
count(5.0);
assert_eq!(p.snapshot().done, 3.0);
}
#[test]
fn check_fails_once_cancelled() {
assert_eq!(check(), Ok(()));
let p = Progress::new();
scoped(Some(p.clone()), || {
assert_eq!(check(), Ok(()));
p.cancel();
assert_eq!(check(), Err(crate::Error::Cancelled));
});
// Not this thread's command any more.
assert_eq!(check(), Ok(()));
}
}