csp_solver/cancel.rs
1//! Cooperative cancellation token.
2//!
3//! A cheap, `Clone`-able flag checked at the same cadence as
4//! [`crate::SolveConfig::node_budget`] inside every search loop
5//! (`solver::backtrack`, `solver::backjump`, `solver::optimize`). Unlike the
6//! node budget — which is an internal, self-imposed cap — this is an
7//! *external* signal: a caller (e.g. the PyO3 boundary, on behalf of an
8//! `asyncio.wait_for` timeout) can flip it from another thread while the
9//! search is running on a `Python::allow_threads`-released worker thread,
10//! and the next node-budget-cadence check will unwind the recursion cleanly.
11//!
12//! Deliberately *not* the same flag/field as `budget_exceeded` — the two are
13//! distinct outcomes (self-imposed cap vs. externally requested stop) and
14//! collapsing them would repeat the exact "conflated failure modes" mistake
15//! flagged elsewhere in this codebase's own solve-result plumbing.
16
17use std::sync::Arc;
18use std::sync::atomic::{AtomicBool, Ordering};
19
20/// A shareable, thread-safe cancellation flag.
21///
22/// `clone()` is cheap (an `Arc` bump) — hand one clone to the search config
23/// and keep another on the calling side to flip when a timeout elapses.
24#[derive(Clone, Debug, Default)]
25pub struct CancelToken(Arc<AtomicBool>);
26
27impl CancelToken {
28 /// Create a new, not-yet-cancelled token.
29 pub fn new() -> Self {
30 Self(Arc::new(AtomicBool::new(false)))
31 }
32
33 /// Request cancellation. Safe to call from any thread, at any time,
34 /// including while a search holding a clone of this token is running.
35 pub fn cancel(&self) {
36 self.0.store(true, Ordering::Relaxed);
37 }
38
39 /// Check whether cancellation has been requested.
40 #[inline]
41 pub fn is_cancelled(&self) -> bool {
42 self.0.load(Ordering::Relaxed)
43 }
44}