1use std::path::{Path, PathBuf};
63use std::sync::atomic::Ordering;
64use std::sync::Arc;
65
66use crate::shared_atomic::{SharedAtomicError, SharedAtomicU64};
67use crate::shared_ring::{RingError, SharedRing, PAYLOAD_BYTES};
68
69pub const MAX_PRIORITIES: usize = 64;
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum FanoutError {
75 Ring(RingError),
76 Atomic(SharedAtomicError),
77 PriorityOutOfBounds,
78 NPrioritiesOutOfBounds,
79 Empty,
80}
81
82impl From<RingError> for FanoutError {
83 fn from(e: RingError) -> Self { Self::Ring(e) }
84}
85impl From<SharedAtomicError> for FanoutError {
86 fn from(e: SharedAtomicError) -> Self { Self::Atomic(e) }
87}
88
89fn bitmap_path(base: &Path) -> PathBuf {
90 let mut p = base.to_path_buf();
91 let stem = p.file_name().unwrap().to_string_lossy().to_string();
92 p.set_file_name(format!("{stem}.bitmap.bin"));
93 p
94}
95fn prio_path(base: &Path, prio: usize) -> PathBuf {
96 let mut p = base.to_path_buf();
97 let stem = p.file_name().unwrap().to_string_lossy().to_string();
98 p.set_file_name(format!("{stem}.prio{prio}.bin"));
99 p
100}
101
102pub struct PriorityFanout {
103 rings: Vec<Arc<SharedRing>>,
104 bitmap: Arc<SharedAtomicU64>,
105 n_priorities: usize,
106 header_sidecar: subetha_core::HandshakeHeader,
107 ring_sidecar: Box<subetha_core::ObservationRing>,
108}
109
110impl subetha_sidecar::AdaptiveInstance for PriorityFanout {
111 fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
112 fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
113 fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
114 Box::new(subetha_sidecar::NoMigrationPolicy)
115 }
116}
117
118impl PriorityFanout {
119 pub fn create(
124 base_path: impl AsRef<Path>,
125 n_priorities: usize,
126 ring_capacity: usize,
127 ) -> Result<Self, FanoutError> {
128 if n_priorities == 0 || n_priorities > MAX_PRIORITIES {
129 return Err(FanoutError::NPrioritiesOutOfBounds);
130 }
131 let base = base_path.as_ref();
132 let mut rings = Vec::with_capacity(n_priorities);
133 for i in 0..n_priorities {
134 rings.push(Arc::new(SharedRing::create(prio_path(base, i), ring_capacity)?));
135 }
136 let bitmap = Arc::new(SharedAtomicU64::create(bitmap_path(base), 0)?);
137 Ok(Self {
138 rings, bitmap, n_priorities,
139 header_sidecar: subetha_core::HandshakeHeader::new(),
140 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
141 })
142 }
143
144 pub fn open(
147 base_path: impl AsRef<Path>,
148 n_priorities: usize,
149 ring_capacity: usize,
150 ) -> Result<Self, FanoutError> {
151 if n_priorities == 0 || n_priorities > MAX_PRIORITIES {
152 return Err(FanoutError::NPrioritiesOutOfBounds);
153 }
154 let base = base_path.as_ref();
155 let mut rings = Vec::with_capacity(n_priorities);
156 for i in 0..n_priorities {
157 rings.push(Arc::new(SharedRing::open(prio_path(base, i), ring_capacity)?));
158 }
159 let bitmap = Arc::new(SharedAtomicU64::open(bitmap_path(base)).map_err(|_| {
160 FanoutError::Atomic(SharedAtomicError::LayoutMismatch)
163 })?);
164 Ok(Self {
165 rings, bitmap, n_priorities,
166 header_sidecar: subetha_core::HandshakeHeader::new(),
167 ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
168 })
169 }
170
171 #[inline]
172 pub fn n_priorities(&self) -> usize { self.n_priorities }
173
174 pub fn submit(&self, priority: usize, payload: &[u8]) -> Result<(), FanoutError> {
178 if priority >= self.n_priorities {
179 self.ring_sidecar
180 .push_op(crate::sidecar_ops::priority_fanout::OP_SUBMIT, 1);
181 return Err(FanoutError::PriorityOutOfBounds);
182 }
183 let r = self.rings[priority].try_push(payload);
184 if r.is_ok() {
185 self.bitmap.fetch_or(1u64 << priority, Ordering::AcqRel);
186 }
187 self.ring_sidecar.push_op(
188 crate::sidecar_ops::priority_fanout::OP_SUBMIT,
189 if r.is_err() { 1 } else { 0 },
190 );
191 r?;
192 Ok(())
193 }
194
195 pub fn try_drain_highest(&self, out: &mut [u8]) -> Result<usize, FanoutError> {
199 for _ in 0..(self.n_priorities * 2) {
204 let bitmap = self.bitmap.load(Ordering::Acquire);
205 if bitmap == 0 {
206 self.ring_sidecar
207 .push_op(crate::sidecar_ops::priority_fanout::OP_DRAIN_HIGHEST, 2);
208 return Err(FanoutError::Empty);
209 }
210 let highest = 63 - bitmap.leading_zeros() as usize;
212 if highest >= self.n_priorities {
213 self.bitmap.fetch_and(!(1u64 << highest), Ordering::AcqRel);
215 continue;
216 }
217 match self.rings[highest].try_pop(out) {
218 Ok(_) => {
219 if self.rings[highest].approx_len() == 0 {
224 self.bitmap.fetch_and(!(1u64 << highest), Ordering::AcqRel);
225 }
226 self.ring_sidecar
227 .push_op(crate::sidecar_ops::priority_fanout::OP_DRAIN_HIGHEST, 0);
228 return Ok(highest);
229 }
230 Err(RingError::Empty) => {
231 self.bitmap.fetch_and(!(1u64 << highest), Ordering::AcqRel);
233 continue;
234 }
235 Err(e) => {
236 self.ring_sidecar
237 .push_op(crate::sidecar_ops::priority_fanout::OP_DRAIN_HIGHEST, 1);
238 return Err(FanoutError::Ring(e));
239 }
240 }
241 }
242 self.ring_sidecar
243 .push_op(crate::sidecar_ops::priority_fanout::OP_DRAIN_HIGHEST, 2);
244 Err(FanoutError::Empty)
245 }
246
247 pub fn try_drain_priority(
251 &self, priority: usize, out: &mut [u8]
252 ) -> Result<(), FanoutError> {
253 if priority >= self.n_priorities {
254 self.ring_sidecar
255 .push_op(crate::sidecar_ops::priority_fanout::OP_DRAIN_PRIORITY, 1);
256 return Err(FanoutError::PriorityOutOfBounds);
257 }
258 let r = self.rings[priority].try_pop(out);
259 if self.rings[priority].approx_len() == 0 {
263 self.bitmap.fetch_and(!(1u64 << priority), Ordering::AcqRel);
264 }
265 self.ring_sidecar.push_op(
266 crate::sidecar_ops::priority_fanout::OP_DRAIN_PRIORITY,
267 if matches!(&r, Err(RingError::Empty)) { 2 } else if r.is_err() { 1 } else { 0 },
268 );
269 r?;
270 Ok(())
271 }
272
273 #[inline]
275 pub fn active_priorities(&self) -> u64 {
276 self.bitmap.load(Ordering::Acquire)
277 }
278
279 pub fn highest_active_priority(&self) -> Option<usize> {
281 let b = self.active_priorities();
282 if b == 0 { return None; }
283 let h = 63 - b.leading_zeros() as usize;
284 if h < self.n_priorities { Some(h) } else { None }
285 }
286
287 pub fn approx_pending(&self, priority: usize) -> Option<usize> {
290 if priority >= self.n_priorities { return None; }
291 Some(self.rings[priority].approx_len())
292 }
293
294 pub fn approx_total_pending(&self) -> usize {
296 self.rings.iter().map(|r| r.approx_len()).sum()
297 }
298
299 pub const PAYLOAD_BYTES: usize = PAYLOAD_BYTES;
300
301 pub fn flush(&self) -> Result<(), FanoutError> {
303 self.bitmap.flush()?;
304 for r in &self.rings { r.flush()?; }
305 Ok(())
306 }
307
308 pub fn flush_async(&self) -> Result<(), FanoutError> {
313 self.bitmap.flush_async()?;
314 for r in &self.rings { r.flush_async()?; }
315 Ok(())
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use std::sync::atomic::{AtomicU32, Ordering as O};
323 use std::sync::Barrier;
324 use std::thread;
325
326 fn tmp_base(name: &str) -> PathBuf {
327 let mut p = std::env::temp_dir();
328 let pid = std::process::id();
329 p.push(format!("subetha-fanout-{name}-{pid}"));
330 p
331 }
332
333 fn cleanup(base: &Path, n: usize) {
334 std::fs::remove_file(bitmap_path(base)).ok();
335 for i in 0..n {
336 std::fs::remove_file(prio_path(base, i)).ok();
337 }
338 }
339
340 fn payload_of(v: u32) -> [u8; PAYLOAD_BYTES] {
341 let mut b = [0u8; PAYLOAD_BYTES];
342 b[0..4].copy_from_slice(&v.to_le_bytes());
343 b
344 }
345 fn unpack(b: &[u8]) -> u32 {
346 u32::from_le_bytes(b[0..4].try_into().unwrap())
347 }
348
349 #[test]
350 fn create_initial_bitmap_is_zero() {
351 let base = tmp_base("init");
352 let f = PriorityFanout::create(&base, 4, 8).unwrap();
353 assert_eq!(f.active_priorities(), 0);
354 assert_eq!(f.highest_active_priority(), None);
355 cleanup(&base, 4);
356 }
357
358 #[test]
359 fn submit_sets_priority_bit() {
360 let base = tmp_base("sub");
361 let f = PriorityFanout::create(&base, 4, 8).unwrap();
362 f.submit(2, &payload_of(42)).unwrap();
363 assert_eq!(f.active_priorities() & 0b0100, 0b0100);
364 assert_eq!(f.highest_active_priority(), Some(2));
365 cleanup(&base, 4);
366 }
367
368 #[test]
369 fn drain_highest_returns_highest_first() {
370 let base = tmp_base("hi-first");
371 let f = PriorityFanout::create(&base, 4, 8).unwrap();
372 f.submit(0, &payload_of(10)).unwrap();
373 f.submit(2, &payload_of(30)).unwrap();
374 f.submit(1, &payload_of(20)).unwrap();
375 f.submit(3, &payload_of(40)).unwrap();
376 let mut buf = [0u8; PAYLOAD_BYTES];
377 let p3 = f.try_drain_highest(&mut buf).unwrap();
378 assert_eq!(p3, 3);
379 assert_eq!(unpack(&buf), 40);
380 let p2 = f.try_drain_highest(&mut buf).unwrap();
381 assert_eq!(p2, 2);
382 assert_eq!(unpack(&buf), 30);
383 let p1 = f.try_drain_highest(&mut buf).unwrap();
384 assert_eq!(p1, 1);
385 let p0 = f.try_drain_highest(&mut buf).unwrap();
386 assert_eq!(p0, 0);
387 assert_eq!(f.try_drain_highest(&mut buf).err(), Some(FanoutError::Empty));
388 cleanup(&base, 4);
389 }
390
391 #[test]
392 fn drain_within_priority_preserves_fifo() {
393 let base = tmp_base("fifo");
394 let f = PriorityFanout::create(&base, 2, 16).unwrap();
395 for i in 0..5u32 { f.submit(1, &payload_of(i)).unwrap(); }
396 for i in 0..5u32 {
397 let mut buf = [0u8; PAYLOAD_BYTES];
398 let p = f.try_drain_highest(&mut buf).unwrap();
399 assert_eq!(p, 1);
400 assert_eq!(unpack(&buf), i);
401 }
402 cleanup(&base, 2);
403 }
404
405 #[test]
406 fn priority_out_of_bounds_rejected() {
407 let base = tmp_base("oob");
408 let f = PriorityFanout::create(&base, 4, 8).unwrap();
409 assert_eq!(f.submit(4, &payload_of(0)).err(),
410 Some(FanoutError::PriorityOutOfBounds));
411 assert_eq!(f.submit(100, &payload_of(0)).err(),
412 Some(FanoutError::PriorityOutOfBounds));
413 cleanup(&base, 4);
414 }
415
416 #[test]
417 fn try_drain_priority_targets_specific_ring() {
418 let base = tmp_base("specific");
419 let f = PriorityFanout::create(&base, 3, 8).unwrap();
420 f.submit(0, &payload_of(100)).unwrap();
421 f.submit(2, &payload_of(300)).unwrap();
422 let mut buf = [0u8; PAYLOAD_BYTES];
423 f.try_drain_priority(0, &mut buf).unwrap();
424 assert_eq!(unpack(&buf), 100);
425 assert_eq!(f.try_drain_priority(1, &mut buf).err(),
427 Some(FanoutError::Ring(RingError::Empty)));
428 f.try_drain_priority(2, &mut buf).unwrap();
430 assert_eq!(unpack(&buf), 300);
431 cleanup(&base, 3);
432 }
433
434 #[test]
435 fn full_ring_returns_error() {
436 let base = tmp_base("full");
437 let f = PriorityFanout::create(&base, 2, 4).unwrap();
438 for i in 0..4u32 { f.submit(0, &payload_of(i)).unwrap(); }
439 match f.submit(0, &payload_of(99)) {
440 Err(FanoutError::Ring(RingError::Full)) => {}
441 other => panic!("expected Ring(Full), got {other:?}"),
442 }
443 cleanup(&base, 2);
444 }
445
446 #[test]
447 fn cross_handle_priority_visible() {
448 let base = tmp_base("cross-handle");
449 let producer = PriorityFanout::create(&base, 4, 8).unwrap();
450 let consumer = PriorityFanout::open(&base, 4, 8).unwrap();
451 producer.submit(3, &payload_of(777)).unwrap();
452 assert_eq!(consumer.highest_active_priority(), Some(3));
453 let mut buf = [0u8; PAYLOAD_BYTES];
454 let p = consumer.try_drain_highest(&mut buf).unwrap();
455 assert_eq!(p, 3);
456 assert_eq!(unpack(&buf), 777);
457 assert_eq!(producer.highest_active_priority(), None);
459 cleanup(&base, 4);
460 }
461
462 #[test]
463 fn highest_priority_drained_first_under_interleaved_submits() {
464 let base = tmp_base("interleave");
465 let f = PriorityFanout::create(&base, 4, 16).unwrap();
466 f.submit(0, &payload_of(1)).unwrap();
468 f.submit(1, &payload_of(2)).unwrap();
469 let mut buf = [0u8; PAYLOAD_BYTES];
470 assert_eq!(f.try_drain_highest(&mut buf).unwrap(), 1);
471 f.submit(3, &payload_of(3)).unwrap();
472 assert_eq!(f.try_drain_highest(&mut buf).unwrap(), 3);
473 f.submit(2, &payload_of(4)).unwrap();
474 assert_eq!(f.try_drain_highest(&mut buf).unwrap(), 2);
475 assert_eq!(f.try_drain_highest(&mut buf).unwrap(), 0);
476 cleanup(&base, 4);
477 }
478
479 #[test]
480 fn concurrent_producers_route_to_correct_priorities() {
481 let base = tmp_base("concurrent-prod");
482 let f = Arc::new(PriorityFanout::create(&base, 4, 256).unwrap());
483 let n_threads = 4;
484 let per_thread = 32;
485 let barrier = Arc::new(Barrier::new(n_threads));
486 let mut handles = vec![];
487 for t in 0..n_threads {
488 let f = f.clone();
489 let barrier = barrier.clone();
490 handles.push(thread::spawn(move || {
491 barrier.wait();
492 for i in 0..per_thread {
494 while f.submit(t, &payload_of(i as u32)).is_err() {
495 std::hint::spin_loop();
496 }
497 }
498 }));
499 }
500 for h in handles { h.join().unwrap(); }
501 let mut counts = [0u32; 4];
503 let mut buf = [0u8; PAYLOAD_BYTES];
504 while let Ok(p) = f.try_drain_highest(&mut buf) {
505 counts[p] += 1;
506 }
507 for c in counts.iter() {
508 assert_eq!(*c, per_thread as u32);
509 }
510 cleanup(&base, 4);
511 }
512
513 #[test]
514 fn observer_sees_bitmap_update_during_workload() {
515 let base = tmp_base("observer-bitmap");
516 let f = Arc::new(PriorityFanout::create(&base, 8, 64).unwrap());
517 f.submit(5, &payload_of(1)).unwrap();
521 let f2 = f.clone();
522 let stop = Arc::new(AtomicU32::new(0));
523 let stop2 = stop.clone();
524 let producer = thread::spawn(move || {
525 for _ in 0..100 {
526 if stop2.load(O::Acquire) == 1 { break; }
527 f2.submit(5, &payload_of(1)).ok();
530 std::thread::yield_now();
531 }
532 });
533 let mut saw = false;
537 for _ in 0..10_000 {
538 if (f.active_priorities() & (1 << 5)) != 0 {
539 saw = true;
540 break;
541 }
542 std::thread::yield_now();
543 }
544 stop.store(1, O::Release);
545 producer.join().unwrap();
546 assert!(saw, "observer never saw bit 5 set");
547 cleanup(&base, 8);
548 }
549
550 #[test]
551 fn n_priorities_out_of_bounds_rejected_at_create() {
552 let base = tmp_base("oob-create");
553 assert_eq!(
554 PriorityFanout::create(&base, 0, 8).err(),
555 Some(FanoutError::NPrioritiesOutOfBounds),
556 );
557 assert_eq!(
558 PriorityFanout::create(&base, MAX_PRIORITIES + 1, 8).err(),
559 Some(FanoutError::NPrioritiesOutOfBounds),
560 );
561 let f = PriorityFanout::create(&base, MAX_PRIORITIES, 4).unwrap();
563 assert_eq!(f.n_priorities(), MAX_PRIORITIES);
564 cleanup(&base, MAX_PRIORITIES);
565 }
566
567 #[test]
568 fn disk_persistence_bitmap_and_rings_survive_reopen() {
569 let base = tmp_base("disk");
570 {
571 let f = PriorityFanout::create(&base, 4, 8).unwrap();
572 f.submit(2, &payload_of(2222)).unwrap();
573 f.submit(0, &payload_of(0000)).unwrap();
574 f.flush().unwrap();
575 }
576 let f2 = PriorityFanout::open(&base, 4, 8).unwrap();
577 assert_eq!(f2.highest_active_priority(), Some(2));
578 let mut buf = [0u8; PAYLOAD_BYTES];
579 let p = f2.try_drain_highest(&mut buf).unwrap();
580 assert_eq!(p, 2);
581 assert_eq!(unpack(&buf), 2222);
582 let p = f2.try_drain_highest(&mut buf).unwrap();
583 assert_eq!(p, 0);
584 assert_eq!(unpack(&buf), 0);
585 cleanup(&base, 4);
586 }
587}