use std::{
future::Future,
sync::atomic::{
AtomicUsize,
Ordering::{AcqRel, Acquire},
},
};
use crossbeam_queue::ArrayQueue;
use crate::{
manage::{Detach, Manage},
slot::SlotMeta,
};
pub enum Parked<M: Manage> {
Accepted,
Refused(M::Connection, SlotMeta),
Destroyed,
}
pub enum Unparked<M: Manage> {
Claimed(M::Connection, SlotMeta),
Empty,
Lost,
}
pub trait Exchange<M: Manage>: Send + Sync + 'static {
fn park(&self, conn: M::Connection, meta: SlotMeta) -> Parked<M>;
fn unpark(&self) -> impl Future<Output = Unparked<M>>;
fn parked(&self) -> u64;
fn clear(&self) {}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoExchange;
impl<M: Manage> Exchange<M> for NoExchange {
fn park(&self, conn: M::Connection, meta: SlotMeta) -> Parked<M> {
Parked::Refused(conn, meta)
}
async fn unpark(&self) -> Unparked<M> {
Unparked::Empty
}
fn parked(&self) -> u64 {
0
}
}
struct Entry<P> {
parked: P,
meta: SlotMeta,
}
pub struct Reservoir<M: Detach> {
queue: ArrayQueue<Entry<M::Parked>>,
admitted: AtomicUsize,
}
impl<M: Detach> Reservoir<M> {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "reservoir capacity must be greater than zero");
Self {
queue: ArrayQueue::new(capacity),
admitted: AtomicUsize::new(0),
}
}
pub fn capacity(&self) -> usize {
self.queue.capacity()
}
pub fn len(&self) -> usize {
self.queue.len()
}
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
fn admit(&self) -> bool {
self.admitted
.fetch_update(AcqRel, Acquire, |n| {
(n < self.queue.capacity()).then_some(n + 1)
})
.is_ok()
}
fn readmit(&self) {
self.admitted.fetch_sub(1, AcqRel);
}
}
impl<M: Detach> Default for Reservoir<M> {
fn default() -> Self {
Self::new(64)
}
}
impl<M: Detach> std::fmt::Debug for Reservoir<M> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Reservoir")
.field("capacity", &self.queue.capacity())
.field("len", &self.queue.len())
.finish()
}
}
impl<M: Detach> Exchange<M> for Reservoir<M> {
fn park(&self, conn: M::Connection, meta: SlotMeta) -> Parked<M> {
if !self.admit() {
return Parked::Refused(conn, meta);
}
let Some(parked) = M::detach(conn) else {
self.readmit();
return Parked::Destroyed;
};
if self.queue.push(Entry { parked, meta }).is_err() {
self.readmit();
return Parked::Destroyed;
}
Parked::Accepted
}
async fn unpark(&self) -> Unparked<M> {
let Some(entry) = self.queue.pop() else {
return Unparked::Empty;
};
self.readmit();
match M::attach(entry.parked).await {
Ok(conn) => Unparked::Claimed(conn, entry.meta),
Err(_) => Unparked::Lost,
}
}
fn parked(&self) -> u64 {
self.queue.len() as u64
}
fn clear(&self) {
while self.queue.pop().is_some() {
self.readmit();
}
}
}
#[cfg(test)]
mod tests {
use std::{
pin::pin,
sync::atomic::Ordering::SeqCst,
task::{Context, Poll, Waker},
};
use super::*;
use crate::test_support::{CAN_ATTACH, CAN_DETACH, MovableConn, MovableManager, detach_lock};
fn drive<F: Future>(fut: F) -> F::Output {
let mut fut = pin!(fut);
match fut.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
Poll::Ready(v) => v,
Poll::Pending => panic!("an exchange future must not yield"),
}
}
fn meta(generation: u64, uses: u64) -> SlotMeta {
let mut m = SlotMeta::new(generation);
m.uses = uses;
m
}
fn reservoir(capacity: usize) -> Reservoir<MovableManager> {
Reservoir::new(capacity)
}
#[test]
fn no_exchange_refuses_and_hands_the_connection_straight_back() {
let x = NoExchange;
match Exchange::<MovableManager>::park(&x, MovableConn::new(7), meta(3, 5)) {
Parked::Refused(conn, meta) => {
assert_eq!(conn.id, 7, "the caller keeps its connection");
assert_eq!(meta.generation, 3);
assert_eq!(meta.uses, 5);
}
_ => panic!("NoExchange must never take ownership"),
}
}
#[test]
fn no_exchange_never_produces_a_connection() {
let x = NoExchange;
assert!(matches!(
drive(Exchange::<MovableManager>::unpark(&x)),
Unparked::Empty
));
assert_eq!(Exchange::<MovableManager>::parked(&x), 0);
}
#[test]
fn no_exchange_clear_is_the_default_no_op() {
Exchange::<MovableManager>::clear(&NoExchange);
}
#[test]
fn no_exchange_is_zero_sized_and_derives_the_usual_traits() {
fn assert_traits<T: Copy + Clone + Default + std::fmt::Debug>() {}
assert_traits::<NoExchange>();
assert_eq!(std::mem::size_of::<NoExchange>(), 0);
assert_eq!(format!("{NoExchange:?}"), "NoExchange");
}
#[test]
fn a_new_reservoir_is_empty() {
let r = reservoir(4);
assert_eq!(r.capacity(), 4);
assert_eq!(r.len(), 0);
assert!(r.is_empty());
assert_eq!(Exchange::<MovableManager>::parked(&r), 0);
}
#[test]
#[should_panic(expected = "reservoir capacity must be greater than zero")]
fn a_zero_capacity_reservoir_panics() {
let _ = reservoir(0);
}
#[test]
fn the_default_reservoir_holds_sixty_four() {
assert_eq!(Reservoir::<MovableManager>::default().capacity(), 64);
}
#[test]
fn debug_reports_capacity_and_occupancy() {
let _lock = detach_lock();
let r = reservoir(4);
r.park(MovableConn::new(0), meta(0, 0));
let s = format!("{r:?}");
assert!(s.contains("capacity: 4"), "got {s}");
assert!(s.contains("len: 1"), "got {s}");
}
#[test]
fn parking_takes_ownership() {
let _lock = detach_lock();
let r = reservoir(2);
assert!(matches!(
r.park(MovableConn::new(1), meta(0, 0)),
Parked::Accepted
));
assert_eq!(r.len(), 1);
assert!(!r.is_empty());
assert_eq!(Exchange::<MovableManager>::parked(&r), 1);
}
#[test]
fn a_full_reservoir_refuses_without_detaching() {
let _lock = detach_lock();
let r = reservoir(2);
r.park(MovableConn::new(0), meta(0, 0));
r.park(MovableConn::new(1), meta(0, 0));
match r.park(MovableConn::new(2), meta(9, 4)) {
Parked::Refused(conn, meta) => {
assert_eq!(conn.id, 2);
assert_eq!((meta.generation, meta.uses), (9, 4));
}
_ => panic!("a full reservoir must refuse"),
}
assert_eq!(r.len(), 2, "and must not grow past capacity");
}
#[test]
fn a_connection_that_cannot_detach_is_reported_destroyed() {
let _lock = detach_lock();
let r = reservoir(2);
CAN_DETACH.store(false, SeqCst);
assert!(matches!(
r.park(MovableConn::new(0), meta(0, 0)),
Parked::Destroyed
));
assert_eq!(r.len(), 0, "nothing was queued");
CAN_DETACH.store(true, SeqCst);
r.park(MovableConn::new(1), meta(0, 0));
r.park(MovableConn::new(2), meta(0, 0));
assert_eq!(r.len(), 2, "failed detaches must not consume capacity");
}
#[test]
fn claiming_an_empty_reservoir_tells_the_caller_to_dial() {
let r = reservoir(2);
assert!(matches!(
drive(Exchange::<MovableManager>::unpark(&r)),
Unparked::Empty
));
}
#[test]
fn a_claimed_connection_keeps_its_identity_and_metadata() {
let _lock = detach_lock();
let r = reservoir(2);
r.park(MovableConn::new(42), meta(3, 8));
match drive(Exchange::<MovableManager>::unpark(&r)) {
Unparked::Claimed(conn, meta) => {
assert_eq!(conn.id, 42, "the same socket, re-wrapped");
assert_eq!(
(meta.generation, meta.uses),
(3, 8),
"lifecycle bookkeeping survives the trip"
);
}
_ => panic!("the parked connection should have been claimed"),
}
assert!(r.is_empty());
}
#[test]
fn claims_are_first_in_first_out() {
let _lock = detach_lock();
let r = reservoir(4);
for id in 0..3 {
r.park(MovableConn::new(id), meta(0, 0));
}
let mut order = Vec::new();
while let Unparked::Claimed(conn, _) = drive(Exchange::<MovableManager>::unpark(&r)) {
order.push(conn.id);
}
assert_eq!(order, vec![0, 1, 2]);
}
#[test]
fn a_connection_that_cannot_reattach_is_reported_lost() {
let _lock = detach_lock();
let r = reservoir(2);
r.park(MovableConn::new(0), meta(0, 0));
CAN_ATTACH.store(false, SeqCst);
assert!(matches!(
drive(Exchange::<MovableManager>::unpark(&r)),
Unparked::Lost
));
assert!(r.is_empty(), "it was popped, not left behind");
}
#[test]
fn claiming_frees_capacity_for_a_later_park() {
let _lock = detach_lock();
let r = reservoir(1);
assert!(matches!(
r.park(MovableConn::new(0), meta(0, 0)),
Parked::Accepted
));
assert!(matches!(
r.park(MovableConn::new(1), meta(0, 0)),
Parked::Refused(..)
));
drive(Exchange::<MovableManager>::unpark(&r));
assert!(
matches!(r.park(MovableConn::new(2), meta(0, 0)), Parked::Accepted),
"the slot freed by the claim must be reusable"
);
}
#[test]
fn a_lost_connection_also_frees_its_capacity() {
let _lock = detach_lock();
let r = reservoir(1);
r.park(MovableConn::new(0), meta(0, 0));
CAN_ATTACH.store(false, SeqCst);
assert!(matches!(
drive(Exchange::<MovableManager>::unpark(&r)),
Unparked::Lost
));
CAN_ATTACH.store(true, SeqCst);
assert!(matches!(
r.park(MovableConn::new(1), meta(0, 0)),
Parked::Accepted
));
}
#[test]
fn clear_drops_everything_and_frees_all_capacity() {
let _lock = detach_lock();
let r = reservoir(2);
r.park(MovableConn::new(0), meta(0, 0));
r.park(MovableConn::new(1), meta(0, 0));
Exchange::<MovableManager>::clear(&r);
assert_eq!(r.len(), 0);
assert!(r.is_empty());
assert_eq!(Exchange::<MovableManager>::parked(&r), 0);
assert!(matches!(
r.park(MovableConn::new(2), meta(0, 0)),
Parked::Accepted
));
assert!(matches!(
r.park(MovableConn::new(3), meta(0, 0)),
Parked::Accepted
));
}
#[test]
fn clearing_an_empty_reservoir_is_harmless() {
let r = reservoir(2);
Exchange::<MovableManager>::clear(&r);
assert!(r.is_empty());
}
#[test]
fn admission_never_exceeds_capacity() {
let _lock = detach_lock();
let r = reservoir(3);
for id in 0..3 {
assert!(matches!(
r.park(MovableConn::new(id), meta(0, 0)),
Parked::Accepted
));
}
for id in 3..6 {
assert!(matches!(
r.park(MovableConn::new(id), meta(0, 0)),
Parked::Refused(..)
));
}
assert_eq!(r.len(), 3);
assert!(r.len() <= r.capacity());
}
}