use std::sync::atomic::{
AtomicU64,
AtomicUsize,
Ordering,
};
use std::time::Instant;
use tokio_util::sync::CancellationToken;
use crate::error::Error;
use crate::mux::{
MuxConfig,
MuxHandle,
};
use crate::stream::Stream;
use crate::transport::{
WsFrameReader,
WsFrameWriter,
};
struct HandleMetrics {
rtt_ns: AtomicU64,
}
impl HandleMetrics {
const fn new() -> Self {
Self {
rtt_ns: AtomicU64::new(1_000_000),
}
}
fn start_sample(&self) -> RttSample<'_> {
RttSample {
metrics: self,
start: Instant::now(),
}
}
fn record_rtt(&self, elapsed_ns: u64) {
let mut prev = self.rtt_ns.load(Ordering::Relaxed);
loop {
let next = if elapsed_ns > prev {
elapsed_ns
} else {
(prev / 10) * 9 + elapsed_ns / 10
};
match self.rtt_ns.compare_exchange_weak(
prev,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return,
Err(actual) => prev = actual,
}
}
}
fn cost(&self, inflight: usize) -> u64 {
let rtt = self.rtt_ns.load(Ordering::Relaxed);
rtt.saturating_mul((inflight as u64).saturating_add(1))
}
}
struct RttSample<'a> {
metrics: &'a HandleMetrics,
start: Instant,
}
impl RttSample<'_> {
fn complete(self) {
let elapsed_ns = self.start.elapsed().as_nanos();
let elapsed_ns = u64::try_from(elapsed_ns).unwrap_or(u64::MAX);
self.metrics.record_rtt(elapsed_ns);
}
}
pub struct Session {
pool: Vec<MuxHandle>,
metrics: Vec<HandleMetrics>,
next: AtomicUsize,
cancel: CancellationToken,
}
impl Session {
pub async fn with_config<W, R>(
connections: Vec<(W, R)>, cancel: CancellationToken, config: MuxConfig,
) -> Result<Self, Error>
where
W: WsFrameWriter + 'static,
R: WsFrameReader + 'static,
{
if connections.is_empty() {
return Err(Error::MuxClosed);
}
let total = connections.len();
let mut pool = Vec::with_capacity(total);
let mut last_error = None;
for (i, (writer, reader)) in connections.into_iter().enumerate() {
match MuxHandle::spawn(writer, reader, cancel.clone(), config.clone()).await {
Ok(mux) => pool.push(mux),
Err(e) => {
tracing::warn!(
index = i,
total,
error = %e,
"SPDY pool: connection {}/{} failed initial PING, skipping",
i + 1,
total,
);
last_error = Some(e);
}
}
}
if pool.is_empty() {
return Err(last_error.unwrap_or(Error::MuxClosed));
}
if pool.len() < total {
tracing::info!(
healthy = pool.len(),
total,
"SPDY pool: proceeding with {}/{} connections",
pool.len(),
total,
);
}
let metrics = (0..pool.len()).map(|_| HandleMetrics::new()).collect();
Ok(Self {
pool,
metrics,
next: AtomicUsize::new(0),
cancel,
})
}
pub async fn open_stream_pair(
&self, error_headers: Vec<(String, String)>, data_headers: Vec<(String, String)>,
) -> Result<Stream, Error> {
let pool_size = self.pool.len();
if pool_size >= 2 {
let (a, b) = self.pick_two(pool_size);
let preferred = if self.handle_cost(a) <= self.handle_cost(b) {
[a, b]
} else {
[b, a]
};
for &idx in &preferred {
if let Some(stream) = self
.try_open(idx, error_headers.clone(), data_headers.clone())
.await?
{
return Ok(stream);
}
}
}
for round in 0..pool_size {
let idx = self.next.fetch_add(1, Ordering::Relaxed) % pool_size;
if let Some(stream) = self
.try_open(idx, error_headers.clone(), data_headers.clone())
.await?
{
return Ok(stream);
}
tracing::debug!(
handle = idx,
round,
"SPDY session: handle unavailable, trying next"
);
}
Err(Error::CapacityExhausted {
in_use: self.in_use(),
limit: self.capacity() as u32,
})
}
async fn try_open(
&self, idx: usize, error_headers: Vec<(String, String)>,
data_headers: Vec<(String, String)>,
) -> Result<Option<Stream>, Error> {
let mux = &self.pool[idx];
if mux.is_closed() {
return Ok(None);
}
let sample = self.metrics[idx].start_sample();
match mux.open_stream_pair(error_headers, data_headers).await {
Ok(stream) => {
sample.complete();
tracing::debug!(
handle = idx,
active = mux.active_pairs(),
cost = self.handle_cost(idx),
"SPDY session: stream opened via P2C"
);
Ok(Some(stream))
}
Err(Error::CapacityExhausted { .. }) => {
Ok(None)
}
Err(e) => Err(e),
}
}
fn handle_cost(&self, idx: usize) -> u64 {
let mux = &self.pool[idx];
if mux.is_closed() {
return u64::MAX;
}
self.metrics[idx].cost(mux.active_pairs())
}
fn pick_two(&self, pool_size: usize) -> (usize, usize) {
let seed = self.next.fetch_add(1, Ordering::Relaxed) as u64;
let a = (seed % pool_size as u64) as usize;
let b = ((seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1)) % pool_size as u64)
as usize;
if a == b {
(a, (a + 1) % pool_size)
} else {
(a, b)
}
}
pub fn capacity(&self) -> usize {
self.pool
.iter()
.filter(|m| !m.is_closed())
.map(|m| m.max_concurrent() as usize)
.sum()
}
pub fn operating_capacity(&self) -> usize {
self.pool
.iter()
.filter(|m| !m.is_closed())
.map(MuxHandle::operating_capacity)
.sum()
}
pub fn in_use(&self) -> usize {
self.pool.iter().map(MuxHandle::active_pairs).sum()
}
pub fn available(&self) -> usize {
self.capacity().saturating_sub(self.in_use())
}
pub fn is_full(&self) -> bool {
self.pool
.iter()
.all(|m| m.is_closed() || m.active_pairs() >= m.max_concurrent() as usize)
}
pub fn is_drained(&self) -> bool {
self.pool.iter().all(MuxHandle::is_closed)
}
pub fn cancellation_token(&self) -> CancellationToken {
self.cancel.clone()
}
pub async fn close(self) -> Result<(), Error> {
self.cancel.cancel();
Ok(())
}
}