use std::collections::VecDeque;
use std::sync::{Condvar, Mutex, PoisonError};
use crate::{ChannelError, ChildMessage};
pub(crate) const DEFAULT_CAPACITY: usize = 1024;
#[derive(Debug)]
struct Inner {
queue: VecDeque<ChildMessage>,
dropped: u64,
closed: bool,
}
#[derive(Debug)]
pub(crate) struct Outbox {
inner: Mutex<Inner>,
capacity: usize,
queued: Condvar,
drained: Condvar,
}
impl Outbox {
pub(crate) fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(Inner {
queue: VecDeque::new(),
dropped: 0,
closed: false,
}),
capacity,
queued: Condvar::new(),
drained: Condvar::new(),
}
}
pub(crate) fn push_lossy(&self, message: ChildMessage) {
let mut inner = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
if inner.closed {
inner.dropped = inner.dropped.saturating_add(1);
return;
}
if self.capacity == 0 {
inner.dropped = inner.dropped.saturating_add(1);
return;
}
if inner.queue.len() >= self.capacity {
let Some(oldest_metric) = inner
.queue
.iter()
.position(|queued| matches!(queued, ChildMessage::Metric { .. }))
else {
inner.dropped = inner.dropped.saturating_add(1);
return;
};
inner.queue.remove(oldest_metric);
inner.dropped = inner.dropped.saturating_add(1);
}
inner.queue.push_back(message);
self.queued.notify_one();
}
pub(crate) fn push_blocking(&self, message: ChildMessage) -> Result<(), ChannelError> {
if self.capacity == 0 {
return Err(ChannelError::Closed);
}
let mut inner = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
while !inner.closed && inner.queue.len() >= self.capacity {
inner = self
.drained
.wait(inner)
.unwrap_or_else(PoisonError::into_inner);
}
if inner.closed {
return Err(ChannelError::Closed);
}
inner.queue.push_back(message);
self.queued.notify_one();
Ok(())
}
pub(crate) fn pop(&self) -> Option<ChildMessage> {
let mut inner = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
while inner.queue.is_empty() && !inner.closed {
inner = self
.queued
.wait(inner)
.unwrap_or_else(PoisonError::into_inner);
}
let taken = inner.queue.pop_front();
if taken.is_some() {
self.drained.notify_one();
}
taken
}
pub(crate) fn close(&self) {
let mut inner = self.inner.lock().unwrap_or_else(PoisonError::into_inner);
inner.closed = true;
drop(inner);
self.queued.notify_all();
self.drained.notify_all();
}
pub(crate) fn is_closed(&self) -> bool {
self.inner
.lock()
.unwrap_or_else(PoisonError::into_inner)
.closed
}
pub(crate) fn dropped(&self) -> u64 {
self.inner
.lock()
.unwrap_or_else(PoisonError::into_inner)
.dropped
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::sync::mpsc;
use std::time::Duration;
use super::*;
const DEADLINE: Duration = Duration::from_secs(5);
fn metric(value: f64) -> ChildMessage {
ChildMessage::Metric {
name: "rps".into(),
value,
}
}
#[test]
fn a_full_outbox_drops_the_oldest_metric_and_counts_it() {
let outbox = Outbox::new(2);
outbox.push_lossy(metric(1.0));
outbox.push_lossy(metric(2.0));
outbox.push_lossy(metric(3.0));
assert_eq!(outbox.dropped(), 1);
assert_eq!(outbox.pop(), Some(metric(2.0)));
assert_eq!(outbox.pop(), Some(metric(3.0)));
}
#[test]
fn a_full_outbox_evicts_a_metric_rather_than_a_readiness_signal() {
let outbox = Outbox::new(3);
outbox
.push_blocking(ChildMessage::Ready)
.expect("room for readiness");
outbox.push_lossy(metric(1.0));
outbox.push_lossy(metric(2.0));
outbox.push_lossy(metric(3.0));
assert_eq!(outbox.dropped(), 1);
assert_eq!(
outbox.pop(),
Some(ChildMessage::Ready),
"readiness was evicted by a metric"
);
assert_eq!(outbox.pop(), Some(metric(2.0)));
assert_eq!(outbox.pop(), Some(metric(3.0)));
}
#[test]
fn a_full_outbox_with_no_metric_to_evict_drops_the_incoming_one() {
let reply = ChildMessage::ActionReply {
action: "gc".to_string(),
body: "ok".to_string(),
id: Some(1),
};
let outbox = Outbox::new(2);
outbox
.push_blocking(ChildMessage::Ready)
.expect("room for readiness");
outbox
.push_blocking(reply.clone())
.expect("room for the reply");
outbox.push_lossy(metric(1.0));
assert_eq!(outbox.dropped(), 1);
assert_eq!(outbox.pop(), Some(ChildMessage::Ready));
assert_eq!(outbox.pop(), Some(reply));
outbox.close();
assert_eq!(
outbox.pop(),
None,
"the incoming metric was queued past capacity"
);
}
#[test]
fn a_must_deliver_push_waits_for_room_and_then_proceeds() {
let outbox = Arc::new(Outbox::new(1));
outbox
.push_blocking(ChildMessage::Ready)
.expect("first fits");
let (tx, rx) = mpsc::channel();
let pusher = Arc::clone(&outbox);
let handle = std::thread::spawn(move || {
let outcome = pusher.push_blocking(ChildMessage::Ready);
tx.send(outcome).expect("report");
});
assert!(
rx.recv_timeout(Duration::from_millis(200)).is_err(),
"push_blocking returned while the outbox was full"
);
assert_eq!(outbox.pop(), Some(ChildMessage::Ready));
rx.recv_timeout(DEADLINE)
.expect("pusher did not proceed")
.expect("push after room");
handle.join().expect("pusher panicked");
}
#[test]
fn closing_releases_a_blocked_push_with_an_error() {
let outbox = Arc::new(Outbox::new(1));
outbox
.push_blocking(ChildMessage::Ready)
.expect("first fits");
let (tx, rx) = mpsc::channel();
let pusher = Arc::clone(&outbox);
let handle = std::thread::spawn(move || {
tx.send(pusher.push_blocking(ChildMessage::Ready))
.expect("report");
});
assert!(
rx.recv_timeout(Duration::from_millis(200)).is_err(),
"returned too early"
);
outbox.close();
let outcome = rx.recv_timeout(DEADLINE).expect("still parked after close");
assert!(matches!(outcome, Err(ChannelError::Closed)));
handle.join().expect("pusher panicked");
}
#[test]
fn a_must_deliver_push_refuses_a_zero_capacity_outbox_rather_than_parking() {
let outbox = Arc::new(Outbox::new(0));
let (tx, rx) = mpsc::channel();
let pusher = Arc::clone(&outbox);
let handle = std::thread::spawn(move || {
tx.send(pusher.push_blocking(ChildMessage::Ready))
.expect("report");
});
let outcome = rx
.recv_timeout(DEADLINE)
.expect("push_blocking parked on a zero-capacity outbox");
assert!(matches!(outcome, Err(ChannelError::Closed)));
handle.join().expect("pusher panicked");
}
#[test]
fn pop_returns_none_once_closed_and_empty() {
let outbox = Outbox::new(4);
outbox.close();
assert_eq!(outbox.pop(), None);
}
#[test]
fn a_lossy_push_after_close_counts_the_drop_and_queues_nothing() {
let outbox = Outbox::new(4);
outbox.close();
outbox.push_lossy(metric(1.0));
assert_eq!(outbox.pop(), None);
assert_eq!(outbox.dropped(), 1);
}
#[test]
fn a_zero_capacity_outbox_counts_the_drop_and_retains_nothing() {
let outbox = Outbox::new(0);
outbox.push_lossy(metric(1.0));
assert_eq!(outbox.dropped(), 1);
outbox.close();
assert_eq!(outbox.pop(), None);
}
}