use std::{
collections::HashMap,
sync::{
Arc, Mutex as StdMutex,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use scion_quic::{h3::client::Http3Client, quic::config::QuicConfig};
use sciparse::address::{ip_addr::ScionIpAddr, ip_socket_addr::ScionSocketIpAddr};
use crate::{
config::Config,
epoch::Network,
error::{AttemptError, Error, TimeoutPhase},
establish::staggered_first_ok,
origin::Origin,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Generation(u64);
impl Generation {
fn next(self) -> Generation {
Generation(self.0.wrapping_add(1))
}
}
pub(crate) struct OriginClient {
origin: Origin,
network: Arc<Network>,
quic: QuicConfig,
attempt_delay: Duration,
connect_timeout: Duration,
current: tokio::sync::Mutex<(Option<Arc<Http3Client>>, Generation)>,
closed: AtomicBool,
last_used: StdMutex<Instant>,
}
impl OriginClient {
fn new(origin: Origin, config: &Config, now: Instant, network: Arc<Network>) -> Self {
let mut quic = config.quic.clone();
if !origin.verify_peer {
quic.verify_peer = false;
quic.cert_verifier = None;
}
OriginClient {
origin,
network,
quic,
attempt_delay: config.connection_attempt_delay,
connect_timeout: config.connect_timeout,
current: tokio::sync::Mutex::new((None, Generation::default())),
closed: AtomicBool::new(false),
last_used: StdMutex::new(now),
}
}
pub(crate) fn touch(&self, now: Instant) {
*self.last_used.lock().expect("last-used lock poisoned") = now;
}
pub(crate) fn last_used(&self) -> Instant {
*self.last_used.lock().expect("last-used lock poisoned")
}
pub(crate) async fn connection(&self) -> Result<(Arc<Http3Client>, Generation), Error> {
let mut current = self.current.lock().await;
self.check_open()?;
if let Some(connection) = ¤t.0 {
return Ok((connection.clone(), current.1));
}
self.establish_and_store(&mut current).await
}
pub(crate) async fn reconnect(
&self,
seen: Generation,
) -> Result<(Arc<Http3Client>, Generation), Error> {
let mut current = self.current.lock().await;
self.check_open()?;
if current.1 != seen
&& let Some(connection) = ¤t.0
{
return Ok((connection.clone(), current.1));
}
self.establish_and_store(&mut current).await
}
pub(crate) async fn close(&self) {
self.closed.store(true, Ordering::Release);
let connection = self.current.lock().await.0.take();
if let Some(connection) = connection {
connection.close().await;
}
}
fn check_open(&self) -> Result<(), Error> {
if self.closed.load(Ordering::Acquire) {
return Err(Error::Closed);
}
Ok(())
}
async fn establish_and_store(
&self,
current: &mut (Option<Arc<Http3Client>>, Generation),
) -> Result<(Arc<Http3Client>, Generation), Error> {
let established =
tokio::time::timeout(self.connect_timeout, self.establish(current.0.as_deref()))
.await
.map_err(|_| {
Error::Timeout {
phase: TimeoutPhase::Connect,
timeout: self.connect_timeout,
}
})??;
if self.closed.load(Ordering::Acquire) {
established.close().await;
return Err(Error::Closed);
}
current.0 = Some(established.clone());
current.1 = current.1.next();
Ok((established, current.1))
}
async fn establish(&self, previous: Option<&Http3Client>) -> Result<Arc<Http3Client>, Error> {
let hosts: Vec<ScionIpAddr> = self.network.resolve(&self.origin.host).await?;
let mut addrs: Vec<ScionSocketIpAddr> = hosts
.iter()
.map(|host| ScionSocketIpAddr::new(host.isd_asn(), host.ip(), self.origin.port))
.collect();
if let Some(previous) = previous {
let last_good = previous.remote();
if let Some(position) = addrs.iter().position(|addr| *addr == last_good) {
addrs.remove(position);
addrs.insert(0, last_good);
}
}
let attempts = addrs.into_iter().map(|addr| self.attempt(addr));
staggered_first_ok(attempts, self.attempt_delay)
.await
.map_err(|errors| {
Error::from_attempt_errors(&self.origin.host, self.origin.port, errors)
})
}
async fn attempt(&self, addr: ScionSocketIpAddr) -> Result<Arc<Http3Client>, AttemptError> {
let socket = self.network.bind().await.map_err(AttemptError::Bind)?;
let client = Http3Client::with_config(
addr,
socket,
Some(self.origin.host.clone()),
self.quic.clone(),
);
client.connect().await.map_err(AttemptError::Establish)?;
Ok(Arc::new(client))
}
}
pub(crate) struct OriginMap {
entries: HashMap<Origin, Arc<OriginClient>>,
max_origins: usize,
idle_timeout: Duration,
}
impl OriginMap {
pub(crate) fn new(max_origins: usize, idle_timeout: Duration) -> Self {
OriginMap {
entries: HashMap::new(),
max_origins: max_origins.max(1),
idle_timeout,
}
}
pub(crate) fn get_or_insert(
&mut self,
origin: Origin,
now: Instant,
config: &Config,
network: &Arc<Network>,
) -> Arc<OriginClient> {
if let Some(client) = self.entries.get(&origin) {
client.touch(now);
return client.clone();
}
self.entries.retain(|origin, client| {
let keep = now.saturating_duration_since(client.last_used()) <= self.idle_timeout;
if !keep {
tracing::trace!(%origin, "Sweeping idle origin");
}
keep
});
if self.entries.len() >= self.max_origins {
let lru = self
.entries
.iter()
.min_by_key(|(_, client)| client.last_used())
.map(|(origin, _)| origin.clone());
if let Some(lru) = lru {
self.entries.remove(&lru);
tracing::debug!(origin = %lru, "Origin cap reached, evicting least recently used");
}
}
let client = Arc::new(OriginClient::new(
origin.clone(),
config,
now,
network.clone(),
));
self.entries.insert(origin, client.clone());
client
}
pub(crate) fn drain(&mut self) -> Vec<Arc<OriginClient>> {
self.entries.drain().map(|(_, client)| client).collect()
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
#[cfg(test)]
pub(crate) fn contains(&self, origin: &Origin) -> bool {
self.entries.contains_key(origin)
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::Ordering::SeqCst;
use test_log::test;
use super::*;
use crate::test_support::{
SERVER_PORT, StaticResolver, TestServerHarness, dead_scion_ip, server_scion_ip,
test_config, test_epoch, test_network, test_router,
};
fn inert_network() -> Arc<Network> {
test_network(
TestServerHarness::new(test_router().0),
StaticResolver::new(vec![]),
)
}
fn at(millis: u64) -> Instant {
static BASE: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
*BASE.get_or_init(Instant::now) + Duration::from_millis(millis)
}
fn dns_origin(host: &str) -> Origin {
Origin {
host: host.to_string(),
port: SERVER_PORT,
verify_peer: true,
}
}
#[test]
fn origin_map_evicts_lru_at_cap() {
let config = test_config();
let network = inert_network();
let mut map = OriginMap::new(2, Duration::from_secs(3600));
let (a, b, c) = (dns_origin("a"), dns_origin("b"), dns_origin("c"));
map.get_or_insert(a.clone(), at(0), &config, &network);
map.get_or_insert(b.clone(), at(10), &config, &network);
map.get_or_insert(a.clone(), at(20), &config, &network);
map.get_or_insert(c.clone(), at(30), &config, &network);
assert_eq!(map.len(), 2);
assert!(map.contains(&a));
assert!(map.contains(&c));
assert!(!map.contains(&b));
}
#[test]
fn origin_map_sweeps_idle_on_insert() {
let config = test_config();
let network = inert_network();
let mut map = OriginMap::new(10, Duration::from_millis(100));
let (a, b, c) = (dns_origin("a"), dns_origin("b"), dns_origin("c"));
map.get_or_insert(a.clone(), at(0), &config, &network);
map.get_or_insert(b.clone(), at(150), &config, &network);
map.get_or_insert(c.clone(), at(300), &config, &network);
assert_eq!(map.len(), 1);
assert!(map.contains(&c));
}
#[test]
fn origin_map_hit_does_not_sweep() {
let config = test_config();
let network = inert_network();
let mut map = OriginMap::new(10, Duration::from_millis(100));
let (a, b) = (dns_origin("a"), dns_origin("b"));
map.get_or_insert(a.clone(), at(0), &config, &network);
map.get_or_insert(b.clone(), at(0), &config, &network);
map.get_or_insert(a.clone(), at(1_000), &config, &network);
assert_eq!(map.len(), 2);
}
#[test(tokio::test)]
#[ntest::timeout(10_000)]
async fn steady_state_reuses_single_connection() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
let config = test_config();
let network = test_network(
harness.clone(),
StaticResolver::new(vec![server_scion_ip()]),
);
let client = OriginClient::new(dns_origin("localhost"), &config, Instant::now(), network);
let (first, generation) = client.connection().await.unwrap();
for _ in 0..4 {
let (next, next_generation) = client.connection().await.unwrap();
assert!(Arc::ptr_eq(&first, &next));
assert_eq!(generation, next_generation);
}
assert_eq!(harness.binds.load(SeqCst), 1);
}
#[test(tokio::test)]
#[ntest::timeout(10_000)]
async fn concurrent_requests_share_one_establishment() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
harness.stall_first_bind();
let config = test_config();
let network = test_network(
harness.clone(),
StaticResolver::new(vec![server_scion_ip()]),
);
let client = Arc::new(OriginClient::new(
dns_origin("localhost"),
&config,
Instant::now(),
network,
));
let tasks: Vec<_> = (0..3)
.map(|_| {
let client = client.clone();
tokio::spawn(async move { client.connection().await })
})
.collect();
tokio::time::sleep(Duration::from_millis(100)).await;
harness.release_stalled();
let mut clients = Vec::new();
for task in tasks {
let (client, generation) = task.await.unwrap().unwrap();
assert_eq!(generation, Generation(1));
clients.push(client);
}
assert!(Arc::ptr_eq(&clients[0], &clients[1]));
assert!(Arc::ptr_eq(&clients[0], &clients[2]));
assert_eq!(harness.binds.load(SeqCst), 1);
}
#[test(tokio::test)]
#[ntest::timeout(10_000)]
async fn reconnect_adopts_peer_established_client() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
let config = test_config();
let network = test_network(
harness.clone(),
StaticResolver::new(vec![server_scion_ip()]),
);
let client = OriginClient::new(dns_origin("localhost"), &config, Instant::now(), network);
let (_, generation) = client.connection().await.unwrap();
assert_eq!(harness.binds.load(SeqCst), 1);
let (fresh, fresh_generation) = client.reconnect(generation).await.unwrap();
assert_eq!(harness.binds.load(SeqCst), 2);
assert_ne!(fresh_generation, generation);
let (adopted, adopted_generation) = client.reconnect(generation).await.unwrap();
assert!(Arc::ptr_eq(&fresh, &adopted));
assert_eq!(adopted_generation, fresh_generation);
assert_eq!(harness.binds.load(SeqCst), 2);
}
#[test(tokio::test)]
#[ntest::timeout(20_000)]
async fn reconnect_tries_last_known_good_first() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
let config = test_config();
let network = test_network(
harness.clone(),
StaticResolver::new(vec![dead_scion_ip(), server_scion_ip()]),
);
let client = OriginClient::new(dns_origin("localhost"), &config, Instant::now(), network);
let (first, generation) = client.connection().await.unwrap();
assert_eq!(first.remote().ip(), server_scion_ip().ip());
assert_eq!(harness.binds.load(SeqCst), 2);
let (fresh, _) = client.reconnect(generation).await.unwrap();
assert_eq!(fresh.remote().ip(), server_scion_ip().ip());
assert_eq!(harness.binds.load(SeqCst), 3);
}
#[test(tokio::test)]
#[ntest::timeout(10_000)]
async fn dns_candidates_resolve_via_epoch_resolver() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
let resolver = StaticResolver::new(vec![server_scion_ip()]);
let config = test_config();
let network = test_network(harness.clone(), resolver.clone());
let client = OriginClient::new(dns_origin("localhost"), &config, Instant::now(), network);
let (_, generation) = client.connection().await.unwrap();
assert_eq!(resolver.calls.load(SeqCst), 1);
client.reconnect(generation).await.unwrap();
assert_eq!(resolver.calls.load(SeqCst), 2);
}
#[test(tokio::test)]
#[ntest::timeout(10_000)]
async fn all_candidates_dead_fails_with_connect() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
let config = test_config();
let network = test_network(harness.clone(), StaticResolver::new(vec![dead_scion_ip()]));
let client = OriginClient::new(dns_origin("localhost"), &config, Instant::now(), network);
let Err(err) = client.connection().await else {
panic!("establishment to a dead candidate succeeded");
};
assert!(matches!(err, Error::Connect { .. }), "{err}");
assert!(err.is_retryable());
}
#[test(tokio::test)]
#[ntest::timeout(10_000)]
async fn closed_origin_client_refuses_to_reconnect() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
let config = test_config();
let network = test_network(
harness.clone(),
StaticResolver::new(vec![server_scion_ip()]),
);
let client = OriginClient::new(dns_origin("localhost"), &config, Instant::now(), network);
let (_, generation) = client.connection().await.unwrap();
client.close().await;
assert!(matches!(client.connection().await, Err(Error::Closed)));
assert!(matches!(
client.reconnect(generation).await,
Err(Error::Closed)
));
assert_eq!(harness.binds.load(SeqCst), 1);
}
#[test(tokio::test)]
#[ntest::timeout(10_000)]
async fn epoch_refuses_origins_after_shutdown() {
let (router, _) = test_router();
let harness = TestServerHarness::new(router);
let config = test_config();
let epoch = test_epoch(
harness.clone(),
StaticResolver::new(vec![server_scion_ip()]),
&config,
);
let origin = dns_origin("localhost");
epoch
.origin_client(origin.clone(), Instant::now(), &config)
.expect("origin client before shutdown");
epoch.clone().shutdown().await;
assert!(matches!(
epoch.origin_client(origin, Instant::now(), &config),
Err(Error::Closed)
));
}
}