use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use tokio::sync::mpsc;
use tokio::sync::Notify;
use crate::driver::DriverCommand;
pub(crate) struct EndpointState {
pub(crate) conns: HashMap<u64, mpsc::WeakUnboundedSender<DriverCommand<Bytes>>>,
pub(crate) live: usize,
pub(crate) closing: bool,
pub(crate) close_frame: Option<(u64, Bytes)>,
pub(crate) next_id: u64,
}
impl EndpointState {
fn new() -> Self {
Self {
conns: HashMap::new(),
live: 0,
closing: false,
close_frame: None,
next_id: 0,
}
}
}
pub(crate) struct EndpointShared {
pub(crate) state: Mutex<EndpointState>,
pub(crate) idle: Notify,
pub(crate) accept_wake: Notify,
}
impl EndpointShared {
pub(crate) fn new() -> Arc<Self> {
Arc::new(Self {
state: Mutex::new(EndpointState::new()),
idle: Notify::new(),
accept_wake: Notify::new(),
})
}
pub(crate) fn is_closing(&self) -> bool {
self.state.lock().unwrap().closing
}
}
pub(crate) fn try_register(
shared: &Arc<EndpointShared>,
cmd_tx: &mpsc::UnboundedSender<DriverCommand<Bytes>>,
) -> Option<ConnRegistration> {
let mut state = shared.state.lock().unwrap();
if state.closing {
return None;
}
let id = state.next_id;
state.next_id += 1;
state.conns.insert(id, cmd_tx.downgrade());
state.live += 1;
Some(ConnRegistration {
shared: Arc::clone(shared),
id,
})
}
pub(crate) struct ConnRegistration {
shared: Arc<EndpointShared>,
id: u64,
}
impl Drop for ConnRegistration {
fn drop(&mut self) {
let became_idle = {
let mut state = self.shared.state.lock().unwrap();
let removed = state.conns.remove(&self.id).is_some();
debug_assert!(
removed,
"ConnRegistration::drop for id {} that was not in the registry",
self.id
);
state.live = state.live.saturating_sub(1);
state.live == 0
};
if became_idle {
self.shared.idle.notify_waiters();
}
}
}
#[derive(Clone)]
pub struct H3QuicheEndpoint(pub(crate) Arc<EndpointShared>);
impl H3QuicheEndpoint {
pub(crate) fn new(shared: Arc<EndpointShared>) -> Self {
Self(shared)
}
pub fn close(&self, code: h3::error::Code, reason: &[u8]) {
let (frame, recipients) = {
let mut state = self.0.state.lock().unwrap();
state.closing = true;
if state.close_frame.is_none() {
state.close_frame = Some((code.value(), Bytes::copy_from_slice(reason)));
}
let frame = state
.close_frame
.clone()
.expect("close_frame was just set above");
let recipients: Vec<mpsc::UnboundedSender<DriverCommand<Bytes>>> =
state.conns.values().filter_map(|w| w.upgrade()).collect();
(frame, recipients)
};
self.0.accept_wake.notify_waiters();
let (code, reason) = frame;
for tx in recipients {
let _ = tx.send(DriverCommand::Close {
code,
reason: reason.clone(),
});
}
}
pub async fn wait_idle(&self) {
loop {
let notified = self.0.idle.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if self.0.state.lock().unwrap().live == 0 {
return;
}
notified.await;
}
}
#[doc(hidden)]
pub fn __test_registry_snapshot(&self) -> (u64, usize) {
let state = self.0.state.lock().unwrap();
(state.next_id, state.live)
}
#[doc(hidden)]
pub fn __test_is_closing(&self) -> bool {
self.0.is_closing()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn frame_of(cmd: &DriverCommand<Bytes>) -> (u64, Bytes) {
match cmd {
DriverCommand::Close { code, reason } => (*code, reason.clone()),
other => panic!("expected DriverCommand::Close, got {other:?}"),
}
}
#[test]
fn close_broadcasts_to_live_workers_and_skips_dead_entries() {
let shared = EndpointShared::new();
let endpoint = H3QuicheEndpoint::new(Arc::clone(&shared));
let (tx_a, mut rx_a) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let (tx_b, mut rx_b) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let (tx_dead, mut rx_dead) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let _reg_a = try_register(&shared, &tx_a).expect("a registers");
let _reg_b = try_register(&shared, &tx_b).expect("b registers");
let _reg_dead = try_register(&shared, &tx_dead).expect("dead registers");
drop(tx_dead);
endpoint.close(h3::error::Code::H3_NO_ERROR, b"bye");
assert_eq!(
frame_of(&rx_a.try_recv().expect("a receives close")),
(
h3::error::Code::H3_NO_ERROR.value(),
Bytes::from_static(b"bye")
),
);
assert_eq!(
frame_of(&rx_b.try_recv().expect("b receives close")),
(
h3::error::Code::H3_NO_ERROR.value(),
Bytes::from_static(b"bye")
),
);
assert!(rx_dead.try_recv().is_err());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dropping_last_guard_wakes_parked_wait_idle() {
let shared = EndpointShared::new();
let endpoint = H3QuicheEndpoint::new(Arc::clone(&shared));
let (tx, _rx) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let reg = try_register(&shared, &tx).expect("registers");
assert_eq!(endpoint.__test_registry_snapshot(), (1, 1));
let waiter = {
let endpoint = endpoint.clone();
tokio::spawn(async move { endpoint.wait_idle().await })
};
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(!waiter.is_finished(), "wait_idle must block while live > 0");
drop(reg);
tokio::time::timeout(Duration::from_secs(2), waiter)
.await
.expect("wait_idle wakes on the 1→0 edge")
.expect("waiter task did not panic");
assert_eq!(endpoint.__test_registry_snapshot(), (1, 0));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn wait_idle_created_before_drop_still_resolves() {
let shared = EndpointShared::new();
let endpoint = H3QuicheEndpoint::new(Arc::clone(&shared));
let (tx, _rx) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let reg = try_register(&shared, &tx).expect("registers");
let fut = endpoint.wait_idle();
tokio::pin!(fut);
assert!(
matches!(futures::poll!(fut.as_mut()), std::task::Poll::Pending),
"wait_idle must park while a worker is still live"
);
drop(reg);
tokio::time::timeout(Duration::from_secs(2), fut)
.await
.expect("no missed notification for a pre-armed wait_idle future");
}
#[tokio::test]
async fn wait_idle_returns_immediately_when_already_idle() {
let shared = EndpointShared::new();
let endpoint = H3QuicheEndpoint::new(shared);
tokio::time::timeout(Duration::from_secs(2), endpoint.wait_idle())
.await
.expect("wait_idle returns immediately at live == 0");
}
#[test]
fn try_register_is_refused_after_close() {
let shared = EndpointShared::new();
let endpoint = H3QuicheEndpoint::new(Arc::clone(&shared));
let (tx_before, mut rx_before) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let _reg_before = try_register(&shared, &tx_before).expect("registers before close");
let (next_id_before, _) = endpoint.__test_registry_snapshot();
endpoint.close(h3::error::Code::H3_NO_ERROR, b"bye");
assert!(
rx_before.try_recv().is_ok(),
"pre-close worker got the close"
);
let (tx_after, _rx_after) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
assert!(
try_register(&shared, &tx_after).is_none(),
"no worker admitted after close()"
);
let (next_id_after, _) = endpoint.__test_registry_snapshot();
assert_eq!(
next_id_before, next_id_after,
"next_id must not advance for a refused registration"
);
}
#[test]
fn close_is_idempotent_first_frame_wins() {
let shared = EndpointShared::new();
let endpoint = H3QuicheEndpoint::new(Arc::clone(&shared));
let (tx, mut rx) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let _reg = try_register(&shared, &tx).expect("registers");
endpoint.close(h3::error::Code::H3_NO_ERROR, b"first");
endpoint.close(h3::error::Code::H3_REQUEST_CANCELLED, b"second");
let first = frame_of(&rx.try_recv().expect("first broadcast"));
assert_eq!(
first,
(
h3::error::Code::H3_NO_ERROR.value(),
Bytes::from_static(b"first")
),
);
let second = frame_of(&rx.try_recv().expect("second broadcast (same frame)"));
assert_eq!(
second,
(
h3::error::Code::H3_NO_ERROR.value(),
Bytes::from_static(b"first")
),
"the second close re-broadcasts the first frame, never its own args"
);
assert!(rx.try_recv().is_err(), "exactly two broadcasts delivered");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn wait_idle_without_close_blocks_then_wakes_on_organic_drain() {
let shared = EndpointShared::new();
let endpoint = H3QuicheEndpoint::new(Arc::clone(&shared));
let (tx, _rx) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let reg = try_register(&shared, &tx).expect("registers");
let waiter = {
let endpoint = endpoint.clone();
tokio::spawn(async move { endpoint.wait_idle().await })
};
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(!waiter.is_finished(), "blocks while a worker is live");
drop(reg);
tokio::time::timeout(Duration::from_secs(2), waiter)
.await
.expect("wait_idle wakes on organic drain")
.expect("waiter task did not panic");
}
#[test]
fn cloned_handles_share_one_registry() {
let shared = EndpointShared::new();
let a = H3QuicheEndpoint::new(Arc::clone(&shared));
let b = a.clone();
assert_eq!(a.__test_registry_snapshot(), (0, 0));
let (tx, _rx) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
let _reg = try_register(&shared, &tx).expect("registers");
assert_eq!(a.__test_registry_snapshot(), (1, 1));
assert_eq!(b.__test_registry_snapshot(), (1, 1));
}
#[test]
fn close_via_one_handle_fences_registration_seen_through_shared() {
let shared = EndpointShared::new();
let a = H3QuicheEndpoint::new(Arc::clone(&shared));
let b = a.clone();
b.close(h3::error::Code::H3_NO_ERROR, b"bye");
let (tx, _rx) = mpsc::unbounded_channel::<DriverCommand<Bytes>>();
assert!(
try_register(&shared, &tx).is_none(),
"close() through any handle fences admission on the shared registry"
);
assert_eq!(a.__test_registry_snapshot(), (0, 0));
}
}