use std::sync::Arc;
use parking_lot::{Condvar, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouterState {
Normal,
Queueing,
}
struct RouterInner {
state: RouterState,
inflight: u64,
}
pub struct WriteRouter {
inner: Mutex<RouterInner>,
drained: Condvar,
#[allow(dead_code)]
state_changed: Condvar,
}
pub struct SyncWriteGuard<'a> {
router: &'a WriteRouter,
}
impl<'a> Drop for SyncWriteGuard<'a> {
fn drop(&mut self) {
let mut g = self.router.inner.lock();
debug_assert!(g.inflight > 0, "SyncWriteGuard drop on zero inflight");
g.inflight -= 1;
if g.inflight == 0 {
self.router.drained.notify_all();
}
}
}
impl WriteRouter {
pub fn new() -> Self {
WriteRouter {
inner: Mutex::new(RouterInner {
state: RouterState::Normal,
inflight: 0,
}),
drained: Condvar::new(),
state_changed: Condvar::new(),
}
}
pub fn state(&self) -> RouterState {
self.inner.lock().state
}
pub fn inflight(&self) -> u64 {
self.inner.lock().inflight
}
pub fn try_enter_sync_writer(&self) -> Option<SyncWriteGuard<'_>> {
let mut g = self.inner.lock();
if g.state == RouterState::Normal {
g.inflight += 1;
Some(SyncWriteGuard { router: self })
} else {
None
}
}
pub fn switch_to_queueing(&self) {
let mut g = self.inner.lock();
g.state = RouterState::Queueing;
self.state_changed.notify_all();
}
pub fn switch_to_normal(&self) {
let mut g = self.inner.lock();
g.state = RouterState::Normal;
self.state_changed.notify_all();
}
pub fn wait_for_no_sync_writers(&self) {
let mut g = self.inner.lock();
while g.inflight > 0 {
self.drained.wait(&mut g);
}
}
pub fn wait_for_no_sync_writers_timeout(&self, timeout: std::time::Duration) -> bool {
use std::time::Instant;
let deadline = Instant::now() + timeout;
let mut g = self.inner.lock();
while g.inflight > 0 {
let now = Instant::now();
if now >= deadline {
return false;
}
let remaining = deadline - now;
let _ = self.drained.wait_for(&mut g, remaining);
}
true
}
}
impl Default for WriteRouter {
fn default() -> Self {
Self::new()
}
}
pub type SharedWriteRouter = Arc<WriteRouter>;
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn new_router_is_normal_with_zero_inflight() {
let r = WriteRouter::new();
assert_eq!(r.state(), RouterState::Normal);
assert_eq!(r.inflight(), 0);
}
#[test]
fn enter_sync_writer_in_normal_returns_guard_and_increments() {
let r = WriteRouter::new();
let g = r.try_enter_sync_writer().expect("Normal must yield guard");
assert_eq!(r.inflight(), 1);
drop(g);
assert_eq!(r.inflight(), 0);
}
#[test]
fn enter_sync_writer_in_queueing_returns_none() {
let r = WriteRouter::new();
r.switch_to_queueing();
assert!(
r.try_enter_sync_writer().is_none(),
"Queueing state must reject sync writers — this is the cutover invariant"
);
assert_eq!(r.inflight(), 0);
}
#[test]
fn inflight_counter_correct_under_multiple_concurrent_writers() {
let r = Arc::new(WriteRouter::new());
let mut handles = vec![];
for _ in 0..10 {
let r_clone = Arc::clone(&r);
handles.push(thread::spawn(move || {
let g = r_clone
.try_enter_sync_writer()
.expect("Normal must yield guard");
thread::sleep(Duration::from_millis(50));
drop(g);
}));
}
thread::sleep(Duration::from_millis(10));
let observed = r.inflight();
assert!(
observed > 0 && observed <= 10,
"expected 1..=10 concurrent inflight writers, got {observed}"
);
for h in handles {
h.join().unwrap();
}
assert_eq!(r.inflight(), 0, "all writers must have released guards");
}
#[test]
fn wait_for_drain_blocks_until_inflight_zero() {
let r = Arc::new(WriteRouter::new());
let writer_done = Arc::new(parking_lot::Mutex::new(false));
let r_clone = Arc::clone(&r);
let done_clone = Arc::clone(&writer_done);
let writer = thread::spawn(move || {
let g = r_clone
.try_enter_sync_writer()
.expect("Normal must yield guard");
thread::sleep(Duration::from_millis(100));
*done_clone.lock() = true;
drop(g);
});
thread::sleep(Duration::from_millis(20));
assert_eq!(r.inflight(), 1);
r.wait_for_no_sync_writers();
assert!(
*writer_done.lock(),
"wait_for_no_sync_writers returned before writer set done flag"
);
writer.join().unwrap();
}
#[test]
fn wait_for_drain_returns_immediately_when_already_zero() {
let r = WriteRouter::new();
let start = std::time::Instant::now();
r.wait_for_no_sync_writers();
assert!(
start.elapsed() < Duration::from_millis(50),
"wait_for must short-circuit when inflight=0"
);
}
#[test]
fn wait_for_timeout_returns_false_when_writers_dont_drain() {
let r = Arc::new(WriteRouter::new());
let _g = r.try_enter_sync_writer().unwrap();
let drained = r.wait_for_no_sync_writers_timeout(Duration::from_millis(50));
assert!(
!drained,
"timeout must return false when writers never drain"
);
}
#[test]
fn cutover_sequence_serializes_writers_and_reembed() {
let r = Arc::new(WriteRouter::new());
let r_a = Arc::clone(&r);
let a_handle = thread::spawn(move || {
let g = r_a.try_enter_sync_writer().expect("A: Normal yields guard");
thread::sleep(Duration::from_millis(200));
drop(g);
});
thread::sleep(Duration::from_millis(20));
assert_eq!(r.inflight(), 1);
r.switch_to_queueing();
assert_eq!(r.state(), RouterState::Queueing);
assert!(
r.try_enter_sync_writer().is_none(),
"B: Queueing must reject sync writers"
);
let drain_start = std::time::Instant::now();
r.wait_for_no_sync_writers();
let drain_dur = drain_start.elapsed();
assert!(
drain_dur >= Duration::from_millis(100),
"wait_for should have blocked >=100ms waiting for A (took {drain_dur:?})"
);
assert_eq!(r.inflight(), 0);
r.switch_to_normal();
assert_eq!(r.state(), RouterState::Normal);
let g_c = r
.try_enter_sync_writer()
.expect("C: post-reembed Normal must yield guard");
assert_eq!(r.inflight(), 1);
drop(g_c);
assert_eq!(r.inflight(), 0);
a_handle.join().unwrap();
}
#[test]
fn guard_drop_is_panic_safe() {
let r = Arc::new(WriteRouter::new());
let r_clone = Arc::clone(&r);
let panicker = thread::spawn(move || {
let _g = r_clone.try_enter_sync_writer().unwrap();
panic!("simulated writer panic mid-write");
});
let res = panicker.join();
assert!(res.is_err(), "writer thread should have panicked");
assert_eq!(
r.inflight(),
0,
"guard Drop must run even on panic — RAII invariant"
);
}
}