axiolid_contracts/
cancel.rs

1//! Cooperative cancellation for long geometry operations.
2//!
3//! # Why cooperative
4//!
5//! `GeomError::Cancelled` existed in the error enum but nothing could produce
6//! it: there was no way to ask for cancellation. The variant was aspirational,
7//! and an aspirational cancellation contract is worse than none, because
8//! callers plan around it.
9//!
10//! This is a plain atomic flag, not an async runtime. Geometry providers are
11//! synchronous and CPU-bound; a token they poll costs one relaxed atomic load
12//! at whatever granularity they declare.
13//!
14//! # Safety of cancellation
15//!
16//! Cancellation is **safe**, never partial. A provider returns either a
17//! complete result or [`GeomError::Cancelled`]. It must never return a
18//! half-cut mesh, because a partial solid is indistinguishable from a valid
19//! one downstream and corrupts every quantity derived from it.
20
21use std::sync::atomic::{AtomicBool, Ordering};
22use std::sync::Arc;
23
24use crate::{GeomError, GeomResult};
25
26/// A shared cancellation flag.
27///
28/// Cloning shares the same flag, so one handle cancels work holding any clone.
29#[derive(Debug, Clone, Default)]
30pub struct CancellationToken {
31    flag: Arc<AtomicBool>,
32}
33
34impl PartialEq for CancellationToken {
35    /// Identity comparison: two handles are equal when they share one flag.
36    ///
37    /// Deliberately not a comparison of current cancelled state, which would
38    /// make two unrelated uncancelled tokens compare equal and then diverge.
39    fn eq(&self, other: &Self) -> bool {
40        Arc::ptr_eq(&self.flag, &other.flag)
41    }
42}
43
44impl Eq for CancellationToken {}
45
46impl CancellationToken {
47    /// A token that is not cancelled.
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Request cancellation. Idempotent, and callable from any thread.
53    pub fn cancel(&self) {
54        self.flag.store(true, Ordering::Relaxed);
55    }
56
57    /// Whether cancellation has been requested.
58    pub fn is_cancelled(&self) -> bool {
59        self.flag.load(Ordering::Relaxed)
60    }
61
62    /// `Err(GeomError::Cancelled)` when cancelled, else `Ok(())`.
63    ///
64    /// The shape providers use at a poll point: `token.check()?`.
65    pub fn check(&self) -> GeomResult<()> {
66        if self.is_cancelled() {
67            return Err(GeomError::Cancelled);
68        }
69        Ok(())
70    }
71}
72
73/// How finely a provider polls its cancellation token.
74///
75/// Declared per provider and asserted by the conformance suite, so a caller
76/// learns the real latency instead of assuming instant cancellation. An honest
77/// coarse granularity beats a false fine one.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79#[non_exhaustive]
80pub enum CancellationGranularity {
81    /// The provider never polls; cancellation has no effect once dispatched.
82    ///
83    /// The correct declaration for a provider wrapping an opaque routine that
84    /// takes no token. It is honest, and it is what lets a caller decide to
85    /// chunk the work itself.
86    None,
87    /// Polled between whole sub-operations, e.g. between tools in a batch.
88    ///
89    /// Latency is bounded by one sub-operation, not by the whole batch.
90    BetweenOperations,
91    /// Polled inside the algorithm's inner loops.
92    Incremental,
93}