use std::fmt;
use std::future::Future;
use std::net::SocketAddr;
use std::time::Duration;
use iroh::TransportAddr;
use iroh::endpoint::{Connection, PathStats};
use tokio::time::{Instant, MissedTickBehavior};
use super::{MAX_PROOFS_PER_CHUNK, VOUCHER_READ_TIMEOUT};
pub(crate) const PROOF_WAIT_CEILING: Duration = Duration::from_secs(30);
const PROGRESS_POLL: Duration = Duration::from_secs(1);
#[derive(Debug)]
pub(super) struct ChunkProofs {
attempts: u32,
started: Instant,
}
impl ChunkProofs {
pub(super) fn start() -> Self {
Self {
attempts: 0,
started: Instant::now(),
}
}
pub(super) const fn next_attempt(&mut self) -> u32 {
self.attempts = self.attempts.saturating_add(1);
self.attempts
}
pub(super) const fn attempts(&self) -> u32 {
self.attempts
}
pub(super) const fn exhausted(&self) -> bool {
self.attempts >= MAX_PROOFS_PER_CHUNK
}
fn expires_at(&self) -> Instant {
self.started + PROOF_WAIT_CEILING
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ProofWaitKind {
Stalled,
PastCeiling,
}
#[derive(Debug)]
pub(super) struct ProofWaitFault {
kind: ProofWaitKind,
detail: String,
}
impl fmt::Display for ProofWaitFault {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.detail)
}
}
impl ProofWaitFault {
pub(super) fn is_past_ceiling(e: &anyhow::Error) -> bool {
e.downcast_ref::<Self>()
.is_some_and(|f| f.kind == ProofWaitKind::PastCeiling)
}
fn into_error(self) -> anyhow::Error {
anyhow::Error::new(super::wire::PeerFault).context(self)
}
}
fn is_stalled(since_progress: Duration) -> bool {
since_progress >= VOUCHER_READ_TIMEOUT
}
struct ProgressClock {
frames_at_start: u64,
frames: u64,
progressed_at: Instant,
}
impl ProgressClock {
const fn new(frames: u64, now: Instant) -> Self {
Self {
frames_at_start: frames,
frames,
progressed_at: now,
}
}
fn observe(&mut self, frames: u64, now: Instant) -> Duration {
if frames != self.frames {
self.frames = frames;
self.progressed_at = now;
}
now.saturating_duration_since(self.progressed_at)
}
const fn frames_sent(&self) -> u64 {
self.frames.saturating_sub(self.frames_at_start)
}
}
pub(super) async fn await_proof<T>(
read: impl Future<Output = anyhow::Result<T>>,
conn: &Connection,
proofs: &ChunkProofs,
) -> anyhow::Result<T> {
wait_for_proof(
read,
proofs,
|| stream_frames_sent(conn),
|| describe_path(conn),
)
.await
}
async fn wait_for_proof<T>(
read: impl Future<Output = anyhow::Result<T>>,
proofs: &ChunkProofs,
stream_frames: impl Fn() -> u64,
path: impl Fn() -> String,
) -> anyhow::Result<T> {
let start = Instant::now();
let mut clock = ProgressClock::new(stream_frames(), start);
let mut poll = tokio::time::interval_at(start + PROGRESS_POLL, PROGRESS_POLL);
poll.set_missed_tick_behavior(MissedTickBehavior::Delay);
let ceiling = tokio::time::sleep_until(proofs.expires_at());
let mut read = std::pin::pin!(read);
let mut ceiling = std::pin::pin!(ceiling);
let (kind, now, since_progress) = loop {
tokio::select! {
biased;
proof = &mut read => return proof,
_ = poll.tick() => {
let now = Instant::now();
let since_progress = clock.observe(stream_frames(), now);
if is_stalled(since_progress) {
break (ProofWaitKind::Stalled, now, since_progress);
}
}
() = &mut ceiling => {
let now = Instant::now();
let since_progress = clock.observe(stream_frames(), now);
break (ProofWaitKind::PastCeiling, now, since_progress);
}
}
};
let waited = now.saturating_duration_since(proofs.started);
let frames = clock.frames_sent();
let detail = match kind {
ProofWaitKind::Stalled => format!(
"no proof and no transport progress for {since_progress:.1?}; the chunk has waited \
{waited:.1?} for its proofs, and this proof wait sent {frames} STREAM frames ({})",
path()
),
ProofWaitKind::PastCeiling => format!(
"the chunk's proofs ran past their wait ceiling after {waited:.1?}; no transport \
progress for {since_progress:.1?}, and this proof wait sent {frames} STREAM frames \
({})",
path()
),
};
Err(ProofWaitFault { kind, detail }.into_error())
}
fn stream_frames_sent(conn: &Connection) -> u64 {
conn.stats().frame_tx.stream
}
fn describe_path(conn: &Connection) -> String {
let (open, selected) = selected_path(conn);
let Some((kind, stats)) = selected else {
return format!("no selected path, {open} open");
};
format!(
"selected path {kind} of {open} open: rtt {:?}, cwnd {} bytes; path lifetime totals: \
{} packets lost, {} congestion events, {} black holes detected",
stats.rtt,
stats.cwnd,
stats.lost_packets,
stats.congestion_events,
stats.black_holes_detected
)
}
pub(super) fn selected_path(conn: &Connection) -> (usize, Option<(&'static str, PathStats)>) {
let paths = conn.paths();
let open = paths.len();
let Some(path) = paths.iter().find(iroh::endpoint::Path::is_selected) else {
return (open, None);
};
let kind = match path.remote_addr() {
TransportAddr::Relay(_) => "relay",
TransportAddr::Ip(SocketAddr::V4(_)) => "direct IPv4",
TransportAddr::Ip(SocketAddr::V6(v6)) if v6.ip().to_canonical().is_ipv4() => "direct IPv4",
TransportAddr::Ip(SocketAddr::V6(_)) => "direct IPv6",
_ => "custom transport",
};
(open, Some((kind, path.stats())))
}
#[cfg(test)]
mod tests;