Skip to main content

mongreldb_core/
execution.rs

1//! Shared cooperative cancellation and deadline control.
2
3use crate::{MongrelError, Result};
4use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9#[repr(u8)]
10pub enum CancellationReason {
11    None = 0,
12    ClientRequest = 1,
13    Deadline = 2,
14    ClientDisconnected = 3,
15    SessionClosed = 4,
16    ServerShutdown = 5,
17}
18
19impl CancellationReason {
20    /// Stable protocol spelling shared by daemon and language bindings.
21    pub const fn as_str(self) -> &'static str {
22        match self {
23            Self::None => "none",
24            Self::ClientRequest => "client_request",
25            Self::Deadline => "deadline",
26            Self::ClientDisconnected => "client_disconnected",
27            Self::SessionClosed => "session_closed",
28            Self::ServerShutdown => "server_shutdown",
29        }
30    }
31
32    pub fn from_protocol_str(value: &str) -> Option<Self> {
33        match value {
34            "none" => Some(Self::None),
35            "client_request" => Some(Self::ClientRequest),
36            "deadline" => Some(Self::Deadline),
37            "client_disconnected" => Some(Self::ClientDisconnected),
38            "session_closed" => Some(Self::SessionClosed),
39            "server_shutdown" => Some(Self::ServerShutdown),
40            _ => None,
41        }
42    }
43
44    fn from_u8(value: u8) -> Self {
45        match value {
46            1 => Self::ClientRequest,
47            2 => Self::Deadline,
48            3 => Self::ClientDisconnected,
49            4 => Self::SessionClosed,
50            5 => Self::ServerShutdown,
51            _ => Self::None,
52        }
53    }
54}
55
56#[derive(Debug)]
57struct CancellationState {
58    sequence: AtomicU64,
59    reason: AtomicU8,
60}
61
62#[derive(Debug, Default)]
63struct CancellationOrder {
64    next_sequence: u64,
65}
66
67/// Cloneable cooperative control shared through one execution.
68///
69/// Child controls inherit every ancestor cancellation state and the tightest
70/// deadline. Cancelling a child does not cancel its parent. Cancellation events
71/// are serialized across the hierarchy, so the first event visible to a control
72/// remains its reason permanently.
73#[derive(Debug, Clone)]
74pub struct ExecutionControl {
75    states: Arc<Vec<Arc<CancellationState>>>,
76    own: Arc<CancellationState>,
77    order: Arc<parking_lot::Mutex<CancellationOrder>>,
78    wake: Arc<tokio::sync::Notify>,
79    deadline: Option<Instant>,
80}
81
82impl ExecutionControl {
83    pub fn new(deadline: Option<Instant>) -> Self {
84        let own = Arc::new(CancellationState {
85            sequence: AtomicU64::new(0),
86            reason: AtomicU8::new(CancellationReason::None as u8),
87        });
88        Self {
89            states: Arc::new(vec![Arc::clone(&own)]),
90            own,
91            order: Arc::new(parking_lot::Mutex::new(CancellationOrder::default())),
92            wake: Arc::new(tokio::sync::Notify::new()),
93            deadline,
94        }
95    }
96
97    pub fn with_timeout(timeout: Duration) -> Self {
98        let now = Instant::now();
99        Self::new(Some(now.checked_add(timeout).unwrap_or(now)))
100    }
101
102    pub fn child_with_deadline(&self, deadline: Option<Instant>) -> Self {
103        let deadline = match (self.deadline, deadline) {
104            (Some(parent), Some(child)) => Some(parent.min(child)),
105            (Some(parent), None) => Some(parent),
106            (None, child) => child,
107        };
108        let own = Arc::new(CancellationState {
109            sequence: AtomicU64::new(0),
110            reason: AtomicU8::new(CancellationReason::None as u8),
111        });
112        let mut states = self.states.as_ref().clone();
113        states.push(Arc::clone(&own));
114        Self {
115            states: Arc::new(states),
116            own,
117            order: Arc::clone(&self.order),
118            wake: Arc::clone(&self.wake),
119            deadline,
120        }
121    }
122
123    pub fn child_with_timeout(&self, timeout: Duration) -> Self {
124        let now = Instant::now();
125        self.child_with_deadline(Some(now.checked_add(timeout).unwrap_or(now)))
126    }
127
128    pub fn cancel(&self, reason: CancellationReason) {
129        if reason == CancellationReason::None {
130            return;
131        }
132        let mut order = self.order.lock();
133        if self.own.reason.load(Ordering::Relaxed) == CancellationReason::None as u8 {
134            self.own
135                .sequence
136                .store(order.next_sequence, Ordering::Relaxed);
137            order.next_sequence = order.next_sequence.saturating_add(1);
138            self.own.reason.store(reason as u8, Ordering::Release);
139        }
140        drop(order);
141        self.wake.notify_waiters();
142    }
143
144    pub fn checkpoint(&self) -> Result<()> {
145        if self
146            .deadline
147            .is_some_and(|deadline| Instant::now() >= deadline)
148        {
149            self.cancel(CancellationReason::Deadline);
150        }
151        match self.reason() {
152            CancellationReason::None => Ok(()),
153            CancellationReason::Deadline => Err(MongrelError::DeadlineExceeded),
154            _ => Err(MongrelError::Cancelled),
155        }
156    }
157
158    pub async fn cancelled(&self) {
159        loop {
160            if self.checkpoint().is_err() {
161                return;
162            }
163            let notified = self.wake.notified();
164            if self.checkpoint().is_err() {
165                return;
166            }
167            if let Some(remaining) = self.remaining_duration() {
168                if tokio::time::timeout(remaining, notified).await.is_err() {
169                    self.cancel(CancellationReason::Deadline);
170                    return;
171                }
172            } else {
173                notified.await;
174            }
175        }
176    }
177
178    pub fn remaining_duration(&self) -> Option<Duration> {
179        self.deadline
180            .map(|deadline| deadline.saturating_duration_since(Instant::now()))
181    }
182
183    pub fn deadline(&self) -> Option<Instant> {
184        self.deadline
185    }
186
187    pub fn reason(&self) -> CancellationReason {
188        self.states
189            .iter()
190            .filter_map(|state| {
191                let reason = CancellationReason::from_u8(state.reason.load(Ordering::Acquire));
192                (reason != CancellationReason::None)
193                    .then(|| (state.sequence.load(Ordering::Relaxed), reason))
194            })
195            .min_by_key(|(sequence, _)| *sequence)
196            .map_or(CancellationReason::None, |(_, reason)| reason)
197    }
198
199    pub fn is_cancelled(&self) -> bool {
200        if self
201            .deadline
202            .is_some_and(|deadline| Instant::now() >= deadline)
203        {
204            self.cancel(CancellationReason::Deadline);
205        }
206        self.reason() != CancellationReason::None
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn first_reason_wins() {
216        let control = ExecutionControl::new(None);
217        control.cancel(CancellationReason::ClientRequest);
218        control.cancel(CancellationReason::ServerShutdown);
219        assert_eq!(control.reason(), CancellationReason::ClientRequest);
220        assert!(matches!(control.checkpoint(), Err(MongrelError::Cancelled)));
221    }
222
223    #[test]
224    fn deadline_returns_structured_error() {
225        let control = ExecutionControl::new(Some(Instant::now()));
226        assert!(matches!(
227            control.checkpoint(),
228            Err(MongrelError::DeadlineExceeded)
229        ));
230        assert_eq!(control.reason(), CancellationReason::Deadline);
231    }
232
233    #[test]
234    fn children_inherit_parent_but_do_not_cancel_it() {
235        let parent = ExecutionControl::new(None);
236        let child = parent.child_with_deadline(None);
237        child.cancel(CancellationReason::ClientRequest);
238        assert!(parent.checkpoint().is_ok());
239        assert!(child.checkpoint().is_err());
240
241        let sibling = parent.child_with_deadline(None);
242        parent.cancel(CancellationReason::SessionClosed);
243        assert_eq!(sibling.reason(), CancellationReason::SessionClosed);
244        assert!(sibling.checkpoint().is_err());
245    }
246
247    #[test]
248    fn child_reason_uses_first_event_without_prior_observation() {
249        let parent = ExecutionControl::new(None);
250        let child = parent.child_with_deadline(None);
251
252        child.cancel(CancellationReason::ClientRequest);
253        parent.cancel(CancellationReason::ServerShutdown);
254
255        assert_eq!(child.reason(), CancellationReason::ClientRequest);
256        assert_eq!(parent.reason(), CancellationReason::ServerShutdown);
257    }
258
259    #[test]
260    fn child_reason_uses_first_parent_event_without_prior_observation() {
261        let parent = ExecutionControl::new(None);
262        let child = parent.child_with_deadline(None);
263
264        parent.cancel(CancellationReason::SessionClosed);
265        child.cancel(CancellationReason::ClientRequest);
266
267        assert_eq!(child.reason(), CancellationReason::SessionClosed);
268    }
269
270    #[test]
271    fn cross_thread_parent_child_order_is_stable() {
272        let parent = ExecutionControl::new(None);
273        let child = parent.child_with_deadline(None);
274        let (cancelled_tx, cancelled_rx) = std::sync::mpsc::sync_channel(0);
275        let child_task = {
276            let child = child.clone();
277            std::thread::spawn(move || {
278                child.cancel(CancellationReason::ClientRequest);
279                cancelled_tx.send(()).unwrap();
280            })
281        };
282
283        cancelled_rx.recv().unwrap();
284        parent.cancel(CancellationReason::ServerShutdown);
285        child_task.join().unwrap();
286
287        assert_eq!(child.reason(), CancellationReason::ClientRequest);
288    }
289
290    #[test]
291    fn concurrent_parent_child_cancellation_stays_stable() {
292        let parent = ExecutionControl::new(None);
293        let child = parent.child_with_deadline(None);
294        let barrier = Arc::new(std::sync::Barrier::new(3));
295        let parent_task = {
296            let parent = parent.clone();
297            let barrier = Arc::clone(&barrier);
298            std::thread::spawn(move || {
299                barrier.wait();
300                parent.cancel(CancellationReason::ServerShutdown);
301            })
302        };
303        let child_task = {
304            let child = child.clone();
305            let barrier = Arc::clone(&barrier);
306            std::thread::spawn(move || {
307                barrier.wait();
308                child.cancel(CancellationReason::ClientRequest);
309            })
310        };
311        barrier.wait();
312        parent_task.join().unwrap();
313        child_task.join().unwrap();
314
315        let observed = child.reason();
316        assert_ne!(observed, CancellationReason::None);
317        parent.cancel(CancellationReason::SessionClosed);
318        child.cancel(CancellationReason::ClientDisconnected);
319        assert_eq!(child.reason(), observed);
320    }
321
322    #[test]
323    fn child_deadline_cannot_weaken_parent() {
324        let parent_deadline = Instant::now() + Duration::from_secs(1);
325        let parent = ExecutionControl::new(Some(parent_deadline));
326        let child = parent.child_with_timeout(Duration::from_secs(60));
327        assert_eq!(child.deadline(), Some(parent_deadline));
328    }
329
330    #[test]
331    fn overflowing_timeouts_fail_closed_without_panicking() {
332        let control = ExecutionControl::with_timeout(Duration::MAX);
333        assert!(matches!(
334            control.checkpoint(),
335            Err(MongrelError::DeadlineExceeded)
336        ));
337
338        let parent = ExecutionControl::new(None);
339        let child = parent.child_with_timeout(Duration::MAX);
340        assert!(matches!(
341            child.checkpoint(),
342            Err(MongrelError::DeadlineExceeded)
343        ));
344    }
345}