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}