use std::collections::HashMap;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use iroh::endpoint::Connection;
use iroh::{Endpoint, EndpointAddr, PublicKey};
use tokio::runtime::Handle;
use tokio_util::task::AbortOnDropHandle;
use tracing::Instrument as _;
use decdn_protocol::ALPN_CLIENT;
use crate::{LocalPullFault, PullTimeout, rate_limited};
pub(crate) async fn dial(
endpoint: &Endpoint,
target: EndpointAddr,
alpn: &'static [u8],
runtime: Option<&Handle>,
span: tracing::Span,
stage: &'static str,
) -> anyhow::Result<Connection> {
let runtime = match runtime {
Some(runtime) => runtime.clone(),
None => Handle::try_current().map_err(|e| {
anyhow::anyhow!("{stage}: no runtime to dial on: {e}").context(LocalPullFault)
})?,
};
let endpoint = endpoint.clone();
let connect = async move { endpoint.connect(target, alpn).await }.instrument(span);
AbortOnDropHandle::new(runtime.spawn(connect))
.await
.map_err(|e| {
let outcome = if e.is_panic() {
"panicked"
} else {
"was cancelled by its runtime shutting down"
};
anyhow::anyhow!("{stage}: the dial task {outcome}").context(LocalPullFault)
})?
.map_err(|e| rate_limited::transport_error(stage, e))
}
#[derive(Debug)]
#[doc(hidden)]
pub struct WarmConnection {
conn: Connection,
}
impl WarmConnection {
pub async fn connect(
endpoint: &Endpoint,
target: EndpointAddr,
open: Duration,
dial_runtime: Option<&Handle>,
) -> anyhow::Result<Self> {
let conn = tokio::time::timeout(
open,
dial(
endpoint,
target,
ALPN_CLIENT,
dial_runtime,
tracing::Span::current(),
"warm connect failed",
),
)
.await
.map_err(|_| anyhow::Error::new(PullTimeout { after: open }))??;
Ok(Self { conn })
}
#[must_use]
#[doc(hidden)]
pub const fn connection(&self) -> &Connection {
&self.conn
}
#[must_use]
#[doc(hidden)]
pub fn is_closed(&self) -> bool {
self.conn.close_reason().is_some()
}
}
#[derive(Debug, Clone)]
pub struct Connections {
inner: Arc<ConnectionsInner>,
}
type Slot = Arc<tokio::sync::Mutex<Option<Arc<WarmConnection>>>>;
#[derive(Debug)]
#[doc(hidden)]
pub enum Lease {
Dialled(Arc<WarmConnection>),
Pinned(Arc<WarmConnection>),
}
impl Lease {
#[must_use]
pub fn into_warm(self) -> Arc<WarmConnection> {
match self {
Self::Dialled(warm) | Self::Pinned(warm) => warm,
}
}
#[must_use]
pub const fn is_pinned(&self) -> bool {
matches!(self, Self::Pinned(_))
}
}
#[derive(Debug)]
struct ConnectionsInner {
endpoint: Endpoint,
alpn: &'static [u8],
dial_runtime: Option<Handle>,
slots: Mutex<HashMap<PublicKey, Slot>>,
#[cfg(any(test, feature = "test-util"))]
dials: std::sync::atomic::AtomicU64,
}
impl Connections {
#[must_use]
pub fn new(endpoint: Endpoint) -> Self {
Self::with_options(endpoint, ALPN_CLIENT, None)
}
#[must_use]
#[doc(hidden)]
pub fn with_options(
endpoint: Endpoint,
alpn: &'static [u8],
dial_runtime: Option<Handle>,
) -> Self {
Self {
inner: Arc::new(ConnectionsInner {
endpoint,
alpn,
dial_runtime,
slots: Mutex::new(HashMap::new()),
#[cfg(any(test, feature = "test-util"))]
dials: std::sync::atomic::AtomicU64::new(0),
}),
}
}
#[must_use]
#[doc(hidden)]
pub fn alpn(&self) -> &'static [u8] {
self.inner.alpn
}
#[doc(hidden)]
pub async fn get(&self, target: &EndpointAddr) -> anyhow::Result<Arc<WarmConnection>> {
self.get_or_dial(target).await.map(Lease::into_warm)
}
#[doc(hidden)]
pub async fn get_or_dial(&self, target: &EndpointAddr) -> anyhow::Result<Lease> {
let slot = Arc::clone(
self.inner
.slots
.lock()
.unwrap_or_else(PoisonError::into_inner)
.entry(target.id)
.or_default(),
);
let mut held = slot.lock().await;
if let Some(warm) = held.as_ref().filter(|warm| !warm.is_closed()) {
return Ok(Lease::Pinned(Arc::clone(warm)));
}
#[cfg(any(test, feature = "test-util"))]
self.inner
.dials
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let span = tracing::debug_span!(
parent: None,
"pooled_connection",
peer = %target.id,
alpn = %String::from_utf8_lossy(self.inner.alpn),
);
span.follows_from(tracing::Span::current());
let conn = dial(
&self.inner.endpoint,
target.clone(),
self.inner.alpn,
self.inner.dial_runtime.as_ref(),
span,
"connect failed",
)
.await?;
let warm = Arc::new(WarmConnection { conn });
*held = Some(Arc::clone(&warm));
Ok(Lease::Dialled(warm))
}
#[doc(hidden)]
pub async fn invalidate(&self, node: PublicKey, conn: &Arc<WarmConnection>) {
let slot = self
.inner
.slots
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(&node)
.map(Arc::clone);
let Some(slot) = slot else {
return;
};
let mut held = slot.lock().await;
if held
.as_ref()
.is_some_and(|pinned| Arc::ptr_eq(pinned, conn))
{
*held = None;
}
}
#[cfg(any(test, feature = "test-util"))]
#[must_use]
pub fn dials(&self) -> u64 {
self.inner.dials.load(std::sync::atomic::Ordering::SeqCst)
}
}
pub(crate) fn is_transport_fault(err: &anyhow::Error, warm: &WarmConnection) -> bool {
warm.is_closed()
|| err
.chain()
.any(|e| e.is::<PullTimeout>() || e.is::<crate::PullStalled>())
}
#[derive(Debug, Clone)]
pub(crate) enum ConnOwner {
Owned,
#[cfg(any(test, feature = "test-util"))]
Borrowed,
Shared {
warm: Arc<WarmConnection>,
map: Connections,
},
}
impl ConnOwner {
pub(crate) const fn owns(&self) -> bool {
matches!(self, Self::Owned)
}
pub(crate) async fn unpin_on_fault(&self, err: &anyhow::Error) {
if let Self::Shared { warm, map } = self
&& is_transport_fault(err, warm)
{
map.invalidate(warm.connection().remote_id(), warm).await;
}
}
}
impl Drop for WarmConnection {
fn drop(&mut self) {
self.conn.close(0u32.into(), b"warm-connection-dropped");
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests;