use std::collections::BTreeMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use iroh::Endpoint;
use tracing::Instrument as _;
use crate::backend::TcpBackend;
use crate::credential::Credential;
use crate::exchange;
use crate::fingerprint;
use crate::lifecycle::{Lifecycle, PeerPath, aggregate};
use crate::peer;
const MAX_CONCURRENT_STREAMS_PER_PEER: usize = 64;
pub(crate) struct ServeState {
pub(crate) endpoint: Endpoint,
pub(crate) credential: Credential,
pub(crate) backend: TcpBackend,
pub(crate) lifecycle: Lifecycle,
peers: Mutex<BTreeMap<u64, PeerPath>>,
next_peer: AtomicU64,
}
impl ServeState {
pub(crate) fn new(endpoint: Endpoint, credential: Credential, backend: TcpBackend) -> Self {
Self {
endpoint,
credential,
backend,
lifecycle: Lifecycle::new(),
peers: Mutex::new(BTreeMap::new()),
next_peer: AtomicU64::new(0),
}
}
fn add_peer(&self, path: PeerPath) -> u64 {
let id = self.next_peer.fetch_add(1, Ordering::Relaxed);
self.with_peers(|peers| {
peers.insert(id, path);
});
id
}
fn remove_peer(&self, id: u64) {
self.with_peers(|peers| {
peers.remove(&id);
});
}
fn with_peers(&self, f: impl FnOnce(&mut BTreeMap<u64, PeerPath>)) {
let mut guard = self
.peers
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
f(&mut guard);
let paths: Vec<PeerPath> = guard.values().copied().collect();
drop(guard);
self.lifecycle.set_status(aggregate(&paths));
}
}
pub(crate) async fn accept_loop(state: std::sync::Arc<ServeState>) {
loop {
let incoming = tokio::select! {
biased;
() = state.lifecycle.wait_until_closed() => break,
incoming = state.endpoint.accept() => incoming,
};
let Some(incoming) = incoming else { break };
let state = state.clone();
tokio::spawn(async move {
match incoming.await {
Ok(connection) => serve_connection(state, connection).await,
Err(error) => tracing::debug!(%error, "a connection never established"),
}
});
}
}
async fn serve_connection(
state: std::sync::Arc<ServeState>,
connection: iroh::endpoint::Connection,
) {
let path = peer::path_of(&connection);
let peer = state.add_peer(path);
let slots = std::sync::Arc::new(tokio::sync::Semaphore::new(MAX_CONCURRENT_STREAMS_PER_PEER));
let span = tracing::info_span!(
"peer",
peer = %fingerprint::of(connection.remote_id().as_bytes()),
path = aggregate(&[path]).as_str(),
);
span.in_scope(|| tracing::info!("peer connected"));
loop {
let accepted = tokio::select! {
biased;
() = state.lifecycle.wait_until_closed() => break,
accepted = connection.accept_bi() => accepted,
};
let Ok((send, recv)) = accepted else { break };
let state = state.clone();
let Ok(slot) = slots.clone().acquire_owned().await else {
break;
};
let guard = state.lifecycle.enter();
tokio::spawn(
async move {
let _slot = slot;
let _guard = guard;
let mut stream = tokio::io::join(recv, send);
let _ =
exchange::serve_exchange(&mut stream, &state.credential, &state.backend).await;
deliver(stream).await;
}
.instrument(span.clone()),
);
}
span.in_scope(|| tracing::info!("peer disconnected"));
state.remove_peer(peer);
}
async fn deliver(stream: tokio::io::Join<iroh::endpoint::RecvStream, iroh::endpoint::SendStream>) {
let (_recv, mut send) = stream.into_inner();
let _ = send.finish();
let _ = send.stopped().await;
}
pub(crate) async fn shutdown(state: &ServeState) {
state.lifecycle.close();
state.lifecycle.wait_until_drained().await;
state.endpoint.close().await;
state.lifecycle.mark_torn_down();
}
pub(crate) async fn shutdown_timeout(state: &ServeState, grace: std::time::Duration) -> bool {
state.lifecycle.close();
let drained = tokio::time::timeout(grace, state.lifecycle.wait_until_drained())
.await
.is_ok();
state.endpoint.close().await;
state.lifecycle.mark_torn_down();
drained
}