Skip to main content

faucet_cli/serve/
registry.rs

1//! In-flight run registry. Tracks per-run cancellation tokens and the queue /
2//! in-flight counters that drive backpressure (429), the `faucet_serve_runs_*`
3//! gauges, `/readyz`, and the shutdown drain. A "queued" run is one that has been
4//! spawned but has not yet acquired an execution permit.
5
6use dashmap::DashMap;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use tokio::sync::Notify;
9use tokio_util::sync::CancellationToken;
10
11pub struct Registry {
12    tokens: DashMap<String, CancellationToken>,
13    queued: AtomicUsize,
14    in_flight: AtomicUsize,
15    max_queued: usize,
16    drained: Notify,
17}
18
19impl Registry {
20    pub fn new(max_queued: usize) -> Self {
21        Self {
22            tokens: DashMap::new(),
23            queued: AtomicUsize::new(0),
24            in_flight: AtomicUsize::new(0),
25            max_queued: max_queued.max(1),
26            drained: Notify::new(),
27        }
28    }
29
30    /// Reserve a queue slot. Returns `true` if a slot was reserved, or `false`
31    /// if the queue is full. Atomic against concurrent submits via a CAS loop
32    /// (`AtomicUsize::try_update`, stabilized in Rust 1.95).
33    pub fn try_reserve(&self) -> bool {
34        self.queued
35            .try_update(Ordering::AcqRel, Ordering::Acquire, |cur| {
36                (cur < self.max_queued).then_some(cur + 1)
37            })
38            .is_ok()
39    }
40
41    /// Release a slot reserved by `try_reserve` that will not be spawned
42    /// (idempotency replay/conflict).
43    pub fn release_reservation(&self) {
44        self.dec_queued();
45    }
46
47    pub fn register(&self, run_id: String, token: CancellationToken) {
48        self.tokens.insert(run_id, token);
49    }
50
51    /// Queued → running: the run acquired its execution permit.
52    pub fn mark_running(&self) {
53        self.dec_queued();
54        self.in_flight.fetch_add(1, Ordering::AcqRel);
55    }
56
57    /// Running transition for a run that never occupied a local queue slot (the
58    /// cluster claim path: `submit` writes Pending + releases its reservation, so
59    /// no queued slot exists to consume). Only bumps `in_flight`.
60    pub fn mark_running_unqueued(&self) {
61        self.in_flight.fetch_add(1, Ordering::AcqRel);
62    }
63
64    /// Running → terminal: drop the token and wake any shutdown drain waiter.
65    pub fn mark_finished(&self, run_id: &str) {
66        self.dec_in_flight();
67        self.tokens.remove(run_id);
68        self.drained.notify_waiters();
69    }
70
71    /// Queued → terminal: a run was cancelled (or the server shut down) while
72    /// still waiting for an execution permit, so it never became in-flight.
73    /// Release its **queue** slot (not `in_flight`), drop the token, and wake any
74    /// drain waiter. Distinct from [`Self::mark_finished`], which decrements
75    /// `in_flight` (#146 R: a cancel on a queued run now takes effect at once).
76    pub fn mark_queued_cancelled(&self, run_id: &str) {
77        self.dec_queued();
78        self.tokens.remove(run_id);
79        self.drained.notify_waiters();
80    }
81
82    /// Saturating decrement of `queued` (never wraps below 0 — a wrap to
83    /// usize::MAX would permanently fail `try_reserve` and wedge the server).
84    fn dec_queued(&self) {
85        let _ = self
86            .queued
87            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |q| {
88                Some(q.saturating_sub(1))
89            });
90    }
91
92    /// Saturating decrement of `in_flight`.
93    fn dec_in_flight(&self) {
94        let _ = self
95            .in_flight
96            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| {
97                Some(n.saturating_sub(1))
98            });
99    }
100
101    /// Cancel a live run. Returns `true` if a live token existed.
102    pub fn cancel(&self, run_id: &str) -> bool {
103        if let Some(t) = self.tokens.get(run_id) {
104            t.cancel();
105            true
106        } else {
107            false
108        }
109    }
110
111    pub fn queued(&self) -> usize {
112        self.queued.load(Ordering::Acquire)
113    }
114
115    pub fn in_flight(&self) -> usize {
116        self.in_flight.load(Ordering::Acquire)
117    }
118
119    pub fn is_full(&self) -> bool {
120        self.queued() >= self.max_queued
121    }
122
123    /// Resolve once no run is queued or in flight. Arms the notification *before*
124    /// re-checking so a transition can't be missed.
125    pub async fn wait_drained(&self) {
126        loop {
127            if self.queued() == 0 && self.in_flight() == 0 {
128                return;
129            }
130            let notified = self.drained.notified();
131            if self.queued() == 0 && self.in_flight() == 0 {
132                return;
133            }
134            notified.await;
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn reserve_respects_capacity() {
145        let r = Registry::new(2);
146        assert!(r.try_reserve());
147        assert!(r.try_reserve());
148        assert!(!r.try_reserve());
149        assert!(r.is_full());
150        r.release_reservation();
151        assert!(r.try_reserve());
152    }
153
154    #[test]
155    fn running_transition_moves_counters() {
156        let r = Registry::new(4);
157        r.try_reserve();
158        assert_eq!(r.queued(), 1);
159        r.mark_running();
160        assert_eq!(r.queued(), 0);
161        assert_eq!(r.in_flight(), 1);
162        r.mark_finished("x");
163        assert_eq!(r.in_flight(), 0);
164    }
165
166    #[test]
167    fn mark_running_unqueued_only_bumps_in_flight() {
168        let r = Registry::new(4);
169        // No reservation taken (cluster claim path).
170        r.mark_running_unqueued();
171        assert_eq!(
172            r.queued(),
173            0,
174            "queued must NOT be decremented (no slot was held)"
175        );
176        assert_eq!(r.in_flight(), 1);
177        r.mark_finished("x");
178        assert_eq!(r.in_flight(), 0);
179        assert_eq!(r.queued(), 0);
180    }
181
182    #[test]
183    fn queued_decrement_saturates_at_zero() {
184        let r = Registry::new(4);
185        // A spurious decrement at 0 must NOT wrap to usize::MAX (that would
186        // permanently fail try_reserve and wedge backpressure — #228).
187        r.mark_running(); // dec_queued() at 0 + in_flight++
188        assert_eq!(r.queued(), 0, "saturating: stays 0, never usize::MAX");
189        assert!(
190            r.try_reserve(),
191            "try_reserve still works (queued not wrapped)"
192        );
193    }
194
195    #[test]
196    fn cancel_reports_presence() {
197        let r = Registry::new(4);
198        let token = CancellationToken::new();
199        r.register("run1".into(), token.clone());
200        assert!(r.cancel("run1"));
201        assert!(token.is_cancelled());
202        assert!(!r.cancel("missing"));
203    }
204
205    #[test]
206    fn is_not_idle_while_queued() {
207        let r = Registry::new(4);
208        r.try_reserve();
209        // queued=1, in_flight=0 — must NOT be considered drained.
210        assert_eq!(r.queued(), 1);
211        assert_eq!(r.in_flight(), 0);
212    }
213
214    #[tokio::test]
215    async fn wait_drained_returns_when_idle() {
216        let r = Registry::new(4);
217        // No in-flight work → resolves immediately.
218        r.wait_drained().await;
219    }
220}