faucet_cli/serve/
registry.rs1use dashmap::DashMap;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use tokio::sync::Notify;
9use tokio_util::sync::CancellationToken;
10
11fn shard_key(run_id: &str, shard_id: &str) -> String {
16 format!("{run_id}::{shard_id}")
17}
18
19pub struct Registry {
20 tokens: DashMap<String, CancellationToken>,
21 queued: AtomicUsize,
22 in_flight: AtomicUsize,
23 max_queued: usize,
24 drained: Notify,
25}
26
27impl Registry {
28 pub fn new(max_queued: usize) -> Self {
29 Self {
30 tokens: DashMap::new(),
31 queued: AtomicUsize::new(0),
32 in_flight: AtomicUsize::new(0),
33 max_queued: max_queued.max(1),
34 drained: Notify::new(),
35 }
36 }
37
38 pub fn try_reserve(&self) -> bool {
42 self.queued
43 .try_update(Ordering::AcqRel, Ordering::Acquire, |cur| {
44 (cur < self.max_queued).then_some(cur + 1)
45 })
46 .is_ok()
47 }
48
49 pub fn release_reservation(&self) {
52 self.dec_queued();
53 }
54
55 pub fn register(&self, run_id: String, token: CancellationToken) {
56 self.tokens.insert(run_id, token);
57 }
58
59 pub fn mark_running(&self) {
61 self.dec_queued();
62 self.in_flight.fetch_add(1, Ordering::AcqRel);
63 }
64
65 pub fn mark_running_unqueued(&self) {
69 self.in_flight.fetch_add(1, Ordering::AcqRel);
70 }
71
72 pub fn mark_finished(&self, run_id: &str) {
74 self.dec_in_flight();
75 self.tokens.remove(run_id);
76 self.drained.notify_waiters();
77 }
78
79 pub fn mark_queued_cancelled(&self, run_id: &str) {
85 self.dec_queued();
86 self.tokens.remove(run_id);
87 self.drained.notify_waiters();
88 }
89
90 fn dec_queued(&self) {
93 let _ = self
94 .queued
95 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |q| {
96 Some(q.saturating_sub(1))
97 });
98 }
99
100 fn dec_in_flight(&self) {
102 let _ = self
103 .in_flight
104 .fetch_update(Ordering::AcqRel, Ordering::Acquire, |n| {
105 Some(n.saturating_sub(1))
106 });
107 }
108
109 pub fn cancel(&self, run_id: &str) -> bool {
111 if let Some(t) = self.tokens.get(run_id) {
112 t.cancel();
113 true
114 } else {
115 false
116 }
117 }
118
119 pub fn register_shard(&self, run_id: &str, shard_id: &str, token: CancellationToken) {
123 self.tokens.insert(shard_key(run_id, shard_id), token);
124 }
125
126 pub fn deregister_shard(&self, run_id: &str, shard_id: &str) {
130 self.tokens.remove(&shard_key(run_id, shard_id));
131 }
132
133 pub fn cancel_run_shards(&self, run_id: &str) -> usize {
138 let prefix = format!("{run_id}::");
139 let mut fired = 0usize;
140 for entry in self.tokens.iter() {
141 if entry.key().starts_with(&prefix) {
142 entry.value().cancel();
143 fired += 1;
144 }
145 }
146 fired
147 }
148
149 pub fn queued(&self) -> usize {
150 self.queued.load(Ordering::Acquire)
151 }
152
153 pub fn in_flight(&self) -> usize {
154 self.in_flight.load(Ordering::Acquire)
155 }
156
157 pub fn is_full(&self) -> bool {
158 self.queued() >= self.max_queued
159 }
160
161 pub async fn wait_drained(&self) {
164 loop {
165 if self.queued() == 0 && self.in_flight() == 0 {
166 return;
167 }
168 let notified = self.drained.notified();
169 if self.queued() == 0 && self.in_flight() == 0 {
170 return;
171 }
172 notified.await;
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn reserve_respects_capacity() {
183 let r = Registry::new(2);
184 assert!(r.try_reserve());
185 assert!(r.try_reserve());
186 assert!(!r.try_reserve());
187 assert!(r.is_full());
188 r.release_reservation();
189 assert!(r.try_reserve());
190 }
191
192 #[test]
193 fn running_transition_moves_counters() {
194 let r = Registry::new(4);
195 r.try_reserve();
196 assert_eq!(r.queued(), 1);
197 r.mark_running();
198 assert_eq!(r.queued(), 0);
199 assert_eq!(r.in_flight(), 1);
200 r.mark_finished("x");
201 assert_eq!(r.in_flight(), 0);
202 }
203
204 #[test]
205 fn mark_running_unqueued_only_bumps_in_flight() {
206 let r = Registry::new(4);
207 r.mark_running_unqueued();
209 assert_eq!(
210 r.queued(),
211 0,
212 "queued must NOT be decremented (no slot was held)"
213 );
214 assert_eq!(r.in_flight(), 1);
215 r.mark_finished("x");
216 assert_eq!(r.in_flight(), 0);
217 assert_eq!(r.queued(), 0);
218 }
219
220 #[test]
221 fn queued_decrement_saturates_at_zero() {
222 let r = Registry::new(4);
223 r.mark_running(); assert_eq!(r.queued(), 0, "saturating: stays 0, never usize::MAX");
227 assert!(
228 r.try_reserve(),
229 "try_reserve still works (queued not wrapped)"
230 );
231 }
232
233 #[test]
234 fn cancel_reports_presence() {
235 let r = Registry::new(4);
236 let token = CancellationToken::new();
237 r.register("run1".into(), token.clone());
238 assert!(r.cancel("run1"));
239 assert!(token.is_cancelled());
240 assert!(!r.cancel("missing"));
241 }
242
243 #[test]
244 fn cancel_run_shards_fires_only_matching_run_tokens() {
245 let r = Registry::new(8);
246 let a0 = CancellationToken::new();
250 let a1 = CancellationToken::new();
251 let b0 = CancellationToken::new();
252 let a_run = CancellationToken::new();
253 r.register_shard("A", "0", a0.clone());
254 r.register_shard("A", "1", a1.clone());
255 r.register_shard("B", "0", b0.clone());
256 r.register("A".into(), a_run.clone());
257
258 let fired = r.cancel_run_shards("A");
259 assert_eq!(fired, 2, "both A shards fired");
260 assert!(a0.is_cancelled());
261 assert!(a1.is_cancelled());
262 assert!(!b0.is_cancelled(), "B's shard untouched");
263 assert!(
264 !a_run.is_cancelled(),
265 "A's whole-run token (bare id, no '::') untouched"
266 );
267
268 assert_eq!(r.cancel_run_shards("C"), 0);
270 }
271
272 #[test]
273 fn deregister_shard_removes_the_token() {
274 let r = Registry::new(4);
275 r.register_shard("A", "0", CancellationToken::new());
276 r.register_shard("A", "1", CancellationToken::new());
277 r.deregister_shard("A", "0");
278 assert_eq!(r.cancel_run_shards("A"), 1, "deregistered token not fired");
280 r.deregister_shard("A", "1");
281 assert_eq!(r.cancel_run_shards("A"), 0, "all shard tokens removed");
282 }
283
284 #[test]
285 fn is_not_idle_while_queued() {
286 let r = Registry::new(4);
287 r.try_reserve();
288 assert_eq!(r.queued(), 1);
290 assert_eq!(r.in_flight(), 0);
291 }
292
293 #[tokio::test]
294 async fn wait_drained_returns_when_idle() {
295 let r = Registry::new(4);
296 r.wait_drained().await;
298 }
299}