use std::sync::atomic::{AtomicU64, Ordering};
use vj_abi::Region;
use crate::sched::{DeferredIoCallback, SchedTime};
pub trait IoSink: Send + Sync {
fn enqueue(&self, at: SchedTime, region: Region, seq: u64, cb: DeferredIoCallback);
}
pub struct DeferIoDispatcher<'a> {
sink: &'a dyn IoSink,
seq: AtomicU64,
}
impl<'a> DeferIoDispatcher<'a> {
pub fn new(sink: &'a dyn IoSink) -> Self {
Self {
sink,
seq: AtomicU64::new(0),
}
}
pub fn defer_io(&self, at: SchedTime, region: Region, cb: DeferredIoCallback) {
let seq = self.seq.fetch_add(1, Ordering::SeqCst);
self.sink.enqueue(at, region, seq, cb);
}
}
pub fn request_finish_as_deferred_effect(
dispatcher: &DeferIoDispatcher<'_>,
at: SchedTime,
region: Region,
code: i32,
on_finish: DeferredIoCallback,
) {
let _ = code;
dispatcher.defer_io(at, region, on_finish);
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
struct GatedFakeSink {
gvt: Mutex<SchedTime>,
pending: Mutex<Vec<(SchedTime, Region, u64, /* fired */ bool)>>,
flush_log: Mutex<Vec<u64>>,
}
impl GatedFakeSink {
fn new(initial_gvt: SchedTime) -> Self {
Self {
gvt: Mutex::new(initial_gvt),
pending: Mutex::new(Vec::new()),
flush_log: Mutex::new(Vec::new()),
}
}
fn advance_gvt(&self, new_gvt: SchedTime) -> usize {
*self.gvt.lock().unwrap() = new_gvt;
let mut pending = self.pending.lock().unwrap();
pending.sort_by_key(|(at, region, seq, _)| (*at, *region as u32, *seq));
let mut flush_log = self.flush_log.lock().unwrap();
let mut fired = 0usize;
for entry in pending.iter_mut() {
let (at, _region, seq, already_fired) = entry;
if !*already_fired && *at <= new_gvt {
*already_fired = true;
flush_log.push(*seq);
fired += 1;
}
}
fired
}
fn entries(&self) -> Vec<(SchedTime, Region, u64, bool)> {
self.pending.lock().unwrap().clone()
}
fn flush_order(&self) -> Vec<u64> {
self.flush_log.lock().unwrap().clone()
}
}
impl IoSink for GatedFakeSink {
fn enqueue(&self, at: SchedTime, region: Region, seq: u64, cb: DeferredIoCallback) {
drop(cb);
self.pending.lock().unwrap().push((at, region, seq, false));
}
}
#[test]
fn defer_io_never_executes_the_callback_synchronously() {
let sink = GatedFakeSink::new(SchedTime { ticks: 0 });
let dispatcher = DeferIoDispatcher::new(&sink);
let fired = Arc::new(AtomicBool::new(false));
let fired_clone = fired.clone();
let cb: DeferredIoCallback = Box::new(move |_ctx| {
fired_clone.store(true, Ordering::SeqCst);
});
dispatcher.defer_io(SchedTime { ticks: 3 }, Region::Active, cb);
assert!(
!fired.load(Ordering::SeqCst),
"defer_io must never invoke its callback synchronously"
);
}
#[test]
fn defer_io_is_a_thin_forward_no_local_buffering() {
assert_eq!(
core::mem::size_of::<DeferIoDispatcher<'static>>(),
core::mem::size_of::<&dyn IoSink>() + core::mem::size_of::<AtomicU64>(),
"DeferIoDispatcher must carry nothing beyond its sink reference and seq counter"
);
let sink = GatedFakeSink::new(SchedTime { ticks: 0 });
let dispatcher = DeferIoDispatcher::new(&sink);
dispatcher.defer_io(SchedTime { ticks: 1 }, Region::Active, Box::new(|_| {}));
dispatcher.defer_io(SchedTime { ticks: 2 }, Region::Active, Box::new(|_| {}));
let entries = sink.entries();
assert_eq!(
entries.len(),
2,
"each defer_io call must reach the sink exactly once, nothing coalesced or dropped locally"
);
assert!(
entries[0].2 < entries[1].2,
"seq must strictly increase across defer_io calls"
);
}
#[test]
fn under_the_degenerate_controller_effects_flush_immediately() {
let now = SchedTime { ticks: 5 };
let sink = GatedFakeSink::new(now);
let dispatcher = DeferIoDispatcher::new(&sink);
dispatcher.defer_io(now, Region::Active, Box::new(|_| {}));
let fired = sink.advance_gvt(now);
assert_eq!(
fired, 1,
"under the degenerate controller gvt==now, so the entry fires as soon as gvt is advanced to now"
);
}
#[test]
fn under_a_speculative_mock_controller_effects_wait_for_gvt_to_pass() {
let sink = GatedFakeSink::new(SchedTime { ticks: 0 });
let dispatcher = DeferIoDispatcher::new(&sink);
dispatcher.defer_io(SchedTime { ticks: 10 }, Region::Active, Box::new(|_| {}));
assert_eq!(
sink.advance_gvt(SchedTime { ticks: 5 }),
0,
"gvt has not yet reached the entry's scheduled tick"
);
assert_eq!(
sink.advance_gvt(SchedTime { ticks: 10 }),
1,
"gvt reaching the entry's tick (>=) is the flush condition"
);
let sink2 = GatedFakeSink::new(SchedTime { ticks: 0 });
let dispatcher2 = DeferIoDispatcher::new(&sink2);
dispatcher2.defer_io(SchedTime { ticks: 20 }, Region::Active, Box::new(|_| {}));
dispatcher2.defer_io(SchedTime { ticks: 20 }, Region::Active, Box::new(|_| {}));
let entries = sink2.entries();
let (first_seq, second_seq) = (entries[0].2, entries[1].2);
assert_eq!(sink2.advance_gvt(SchedTime { ticks: 20 }), 2);
assert_eq!(
sink2.flush_order(),
vec![first_seq, second_seq],
"two entries at the same tick must flush in seq order"
);
}
#[test]
fn request_finish_does_not_halt_other_same_timestep_activations() {
let sink = GatedFakeSink::new(SchedTime { ticks: 0 });
let dispatcher = DeferIoDispatcher::new(&sink);
let now = SchedTime { ticks: 4 };
request_finish_as_deferred_effect(&dispatcher, now, Region::Active, 0, Box::new(|_| {}));
assert_eq!(
sink.entries().len(),
1,
"request_finish must enqueue exactly one deferred entry, not execute synchronously"
);
dispatcher.defer_io(now, Region::Active, Box::new(|_| {}));
dispatcher.defer_io(now, Region::Active, Box::new(|_| {}));
assert_eq!(
sink.entries().len(),
3,
"unrelated defer_io calls after request_finish must still enqueue normally"
);
}
}