1use 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 mark_shard_running(&self) {
140 self.in_flight.fetch_add(1, Ordering::AcqRel);
141 }
142
143 pub fn mark_shard_finished(&self, run_id: &str, shard_id: &str) {
147 self.dec_in_flight();
148 self.deregister_shard(run_id, shard_id);
149 self.drained.notify_waiters();
150 }
151
152 pub fn cancel_run_shards(&self, run_id: &str) -> usize {
157 let prefix = format!("{run_id}::");
158 let mut fired = 0usize;
159 for entry in self.tokens.iter() {
160 if entry.key().starts_with(&prefix) {
161 entry.value().cancel();
162 fired += 1;
163 }
164 }
165 fired
166 }
167
168 pub fn queued(&self) -> usize {
169 self.queued.load(Ordering::Acquire)
170 }
171
172 pub fn in_flight(&self) -> usize {
173 self.in_flight.load(Ordering::Acquire)
174 }
175
176 pub fn is_full(&self) -> bool {
177 self.queued() >= self.max_queued
178 }
179
180 pub async fn wait_drained(&self) {
183 loop {
184 if self.queued() == 0 && self.in_flight() == 0 {
185 return;
186 }
187 let notified = self.drained.notified();
188 if self.queued() == 0 && self.in_flight() == 0 {
189 return;
190 }
191 notified.await;
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn reserve_respects_capacity() {
202 let r = Registry::new(2);
203 assert!(r.try_reserve());
204 assert!(r.try_reserve());
205 assert!(!r.try_reserve());
206 assert!(r.is_full());
207 r.release_reservation();
208 assert!(r.try_reserve());
209 }
210
211 #[test]
212 fn running_transition_moves_counters() {
213 let r = Registry::new(4);
214 r.try_reserve();
215 assert_eq!(r.queued(), 1);
216 r.mark_running();
217 assert_eq!(r.queued(), 0);
218 assert_eq!(r.in_flight(), 1);
219 r.mark_finished("x");
220 assert_eq!(r.in_flight(), 0);
221 }
222
223 #[test]
224 fn mark_running_unqueued_only_bumps_in_flight() {
225 let r = Registry::new(4);
226 r.mark_running_unqueued();
228 assert_eq!(
229 r.queued(),
230 0,
231 "queued must NOT be decremented (no slot was held)"
232 );
233 assert_eq!(r.in_flight(), 1);
234 r.mark_finished("x");
235 assert_eq!(r.in_flight(), 0);
236 assert_eq!(r.queued(), 0);
237 }
238
239 #[test]
240 fn queued_decrement_saturates_at_zero() {
241 let r = Registry::new(4);
242 r.mark_running(); assert_eq!(r.queued(), 0, "saturating: stays 0, never usize::MAX");
246 assert!(
247 r.try_reserve(),
248 "try_reserve still works (queued not wrapped)"
249 );
250 }
251
252 #[test]
253 fn cancel_reports_presence() {
254 let r = Registry::new(4);
255 let token = CancellationToken::new();
256 r.register("run1".into(), token.clone());
257 assert!(r.cancel("run1"));
258 assert!(token.is_cancelled());
259 assert!(!r.cancel("missing"));
260 }
261
262 #[test]
263 fn cancel_run_shards_fires_only_matching_run_tokens() {
264 let r = Registry::new(8);
265 let a0 = CancellationToken::new();
269 let a1 = CancellationToken::new();
270 let b0 = CancellationToken::new();
271 let a_run = CancellationToken::new();
272 r.register_shard("A", "0", a0.clone());
273 r.register_shard("A", "1", a1.clone());
274 r.register_shard("B", "0", b0.clone());
275 r.register("A".into(), a_run.clone());
276
277 let fired = r.cancel_run_shards("A");
278 assert_eq!(fired, 2, "both A shards fired");
279 assert!(a0.is_cancelled());
280 assert!(a1.is_cancelled());
281 assert!(!b0.is_cancelled(), "B's shard untouched");
282 assert!(
283 !a_run.is_cancelled(),
284 "A's whole-run token (bare id, no '::') untouched"
285 );
286
287 assert_eq!(r.cancel_run_shards("C"), 0);
289 }
290
291 #[test]
292 fn shard_running_counts_toward_in_flight_and_drain() {
293 let r = Registry::new(4);
297 r.register_shard("A", "0", CancellationToken::new());
298 r.mark_shard_running();
299 assert_eq!(r.in_flight(), 1, "a running shard bumps in_flight");
300 r.mark_shard_finished("A", "0");
302 assert_eq!(r.in_flight(), 0);
303 assert_eq!(
304 r.cancel_run_shards("A"),
305 0,
306 "the shard token was removed on finish"
307 );
308 }
309
310 #[test]
311 fn deregister_shard_removes_the_token() {
312 let r = Registry::new(4);
313 r.register_shard("A", "0", CancellationToken::new());
314 r.register_shard("A", "1", CancellationToken::new());
315 r.deregister_shard("A", "0");
316 assert_eq!(r.cancel_run_shards("A"), 1, "deregistered token not fired");
318 r.deregister_shard("A", "1");
319 assert_eq!(r.cancel_run_shards("A"), 0, "all shard tokens removed");
320 }
321
322 #[test]
323 fn is_not_idle_while_queued() {
324 let r = Registry::new(4);
325 r.try_reserve();
326 assert_eq!(r.queued(), 1);
328 assert_eq!(r.in_flight(), 0);
329 }
330
331 #[tokio::test]
332 async fn wait_drained_returns_when_idle() {
333 let r = Registry::new(4);
334 r.wait_drained().await;
336 }
337}