use s2n_quic_core::inet::{SocketAddress, SocketAddressV6};
use std::{
collections::hash_map::RandomState, hash::BuildHasher, mem::ManuallyDrop, net::SocketAddr,
sync::Arc, time::Duration,
};
use tokio::{runtime::Runtime, sync::Semaphore, task::JoinHandle, time::Instant};
pub(super) struct RehandshakeState {
queue: Vec<SocketAddressV6>,
handshake_at: Option<Instant>,
schedule_handshake_at: Instant,
rehandshake_period: Duration,
hasher: RandomState,
runtime: ManuallyDrop<Runtime>,
semaphore: Arc<Semaphore>,
}
impl RehandshakeState {
pub(super) fn new(rehandshake_period: Duration) -> std::io::Result<Self> {
Self::new_with_runtime(rehandshake_period, false)
}
#[cfg(test)]
fn new_with_paused_time(rehandshake_period: Duration) -> std::io::Result<Self> {
Self::new_with_runtime(rehandshake_period, true)
}
fn new_with_runtime(rehandshake_period: Duration, start_paused: bool) -> std::io::Result<Self> {
let mut builder = tokio::runtime::Builder::new_current_thread();
builder.enable_all();
if start_paused {
#[cfg(test)]
builder.start_paused(true);
}
let runtime = builder.build()?;
let _guard = runtime.enter();
let now = Instant::now();
Ok(Self {
queue: Default::default(),
handshake_at: Default::default(),
schedule_handshake_at: now,
rehandshake_period,
hasher: RandomState::new(),
semaphore: Arc::new(Semaphore::new(2)),
runtime: ManuallyDrop::new(runtime),
})
}
pub(super) fn needs_refill(&mut self) -> bool {
self.queue.is_empty()
}
pub(super) fn rehandshake_period(&self) -> Duration {
self.rehandshake_period
}
pub(super) fn push(&mut self, peer: SocketAddr) {
self.queue.push(SocketAddress::from(peer).to_ipv6_mapped());
}
pub(super) fn adjust_post_refill(&mut self) {
self.queue
.sort_unstable_by_key(|peer| (self.hasher.hash_one(peer), *peer));
self.queue.dedup();
}
pub(super) fn reserve(&mut self, capacity: usize) {
self.queue.reserve(capacity);
}
pub(super) fn next_rehandshake_batch(
&mut self,
peer_count: usize,
mut request_handshake: impl FnMut(SocketAddr) -> Option<JoinHandle<()>>,
) -> usize {
let _guard = self.runtime.enter();
let start = Instant::now();
let batch_deadline = start + Duration::from_secs(50);
let mut to_select =
(60.0 * peer_count as f64 / self.rehandshake_period.as_secs() as f64).trunc() as usize;
let mut max_delay =
(self.rehandshake_period.as_secs() as f64 / peer_count as f64).ceil() as u64;
if self.handshake_at.is_none() && max_delay > 0 && self.schedule_handshake_at <= start {
max_delay = max_delay.clamp(0, self.rehandshake_period.as_secs());
let delta = rand::random_range(0..max_delay);
self.handshake_at = Some(start + Duration::from_secs(delta));
self.schedule_handshake_at = start + Duration::from_secs(max_delay);
}
if self.handshake_at.is_some_and(|t| t <= start) {
to_select += 1;
self.handshake_at = None;
}
let mut handles = Vec::new();
let mut last_spawn = start;
while to_select > 0 {
let now = Instant::now();
if now >= batch_deadline {
break;
}
to_select -= 1;
let Some(entry) = self.queue.pop() else {
to_select = 0;
break;
};
let pace_until = last_spawn + Duration::from_millis(100);
self.runtime.block_on(tokio::time::sleep_until(pace_until));
last_spawn = Instant::now();
#[expect(
clippy::unwrap_used,
reason = "acquire_owned only errors when the semaphore is closed, and this semaphore is never closed"
)]
let permit = self
.runtime
.block_on(self.semaphore.clone().acquire_owned())
.unwrap();
if let Some(handle) = request_handshake(entry.unmap().into()) {
let wrapped = self.runtime.spawn(async move {
if let Err(err) = handle.await {
if let Ok(panic) = err.try_into_panic() {
std::panic::resume_unwind(panic);
}
}
drop(permit);
});
handles.push(wrapped);
} else {
drop(permit);
}
}
for handle in handles {
if let Err(err) = self.runtime.block_on(handle) {
if let Ok(panic) = err.try_into_panic() {
std::panic::resume_unwind(panic);
}
}
}
to_select
}
}
impl Drop for RehandshakeState {
fn drop(&mut self) {
unsafe {
ManuallyDrop::take(&mut self.runtime).shutdown_background();
}
}
}
#[cfg(test)]
mod test;