use std::{
collections::{hash_map::Entry, HashMap, HashSet},
fmt,
num::NonZeroUsize,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::Duration,
};
use futures_lite::{future::BoxedLocal, Stream, StreamExt};
use hashlink::LinkedHashSet;
use iroh_base::hash::{BlobFormat, Hash, HashAndFormat};
use iroh_net::{MagicEndpoint, NodeAddr, NodeId};
use tokio::{
sync::{mpsc, oneshot},
task::JoinSet,
};
use tokio_util::{sync::CancellationToken, task::LocalPoolHandle, time::delay_queue};
use tracing::{debug, error_span, trace, warn, Instrument};
use crate::{
get::{db::DownloadProgress, Stats},
store::Store,
util::{progress::ProgressSender, SetTagOption, TagSet},
TempTag,
};
mod get;
mod invariants;
mod progress;
mod test;
use self::progress::{BroadcastProgressSender, ProgressSubscriber, ProgressTracker};
const IDLE_PEER_TIMEOUT: Duration = Duration::from_secs(10);
const SERVICE_CHANNEL_CAPACITY: usize = 128;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, derive_more::Display)]
pub struct IntentId(pub u64);
pub trait Dialer: Stream<Item = (NodeId, anyhow::Result<Self::Connection>)> + Unpin {
type Connection: Clone;
fn queue_dial(&mut self, node_id: NodeId);
fn pending_count(&self) -> usize;
fn is_pending(&self, node: &NodeId) -> bool;
}
#[derive(Debug)]
pub enum FailureAction {
AllIntentsDropped,
AbortRequest(anyhow::Error),
DropPeer(anyhow::Error),
RetryLater(anyhow::Error),
}
type GetFut = BoxedLocal<InternalDownloadResult>;
pub trait Getter {
type Connection;
fn get(
&mut self,
kind: DownloadKind,
conn: Self::Connection,
progress_sender: BroadcastProgressSender,
) -> GetFut;
}
#[derive(Debug)]
pub struct ConcurrencyLimits {
pub max_concurrent_requests: usize,
pub max_concurrent_requests_per_node: usize,
pub max_open_connections: usize,
pub max_concurrent_dials_per_hash: usize,
}
impl Default for ConcurrencyLimits {
fn default() -> Self {
ConcurrencyLimits {
max_concurrent_requests: 50,
max_concurrent_requests_per_node: 4,
max_open_connections: 25,
max_concurrent_dials_per_hash: 5,
}
}
}
impl ConcurrencyLimits {
fn at_requests_capacity(&self, active_requests: usize) -> bool {
active_requests >= self.max_concurrent_requests
}
fn node_at_request_capacity(&self, active_node_requests: usize) -> bool {
active_node_requests >= self.max_concurrent_requests_per_node
}
fn at_connections_capacity(&self, active_connections: usize) -> bool {
active_connections >= self.max_open_connections
}
fn at_dials_per_hash_capacity(&self, concurrent_dials: usize) -> bool {
concurrent_dials >= self.max_concurrent_dials_per_hash
}
}
#[derive(Debug)]
pub struct RetryConfig {
pub max_retries_per_node: u32,
pub initial_retry_delay: Duration,
}
impl Default for RetryConfig {
fn default() -> Self {
Self {
max_retries_per_node: 6,
initial_retry_delay: Duration::from_millis(500),
}
}
}
#[derive(Debug, Clone)]
pub struct DownloadRequest {
kind: DownloadKind,
nodes: Vec<NodeAddr>,
tag: Option<SetTagOption>,
progress: Option<ProgressSubscriber>,
}
impl DownloadRequest {
pub fn new(
resource: impl Into<DownloadKind>,
nodes: impl IntoIterator<Item = impl Into<NodeAddr>>,
) -> Self {
Self {
kind: resource.into(),
nodes: nodes.into_iter().map(|n| n.into()).collect(),
tag: Some(SetTagOption::Auto),
progress: None,
}
}
pub fn untagged(
resource: HashAndFormat,
nodes: impl IntoIterator<Item = impl Into<NodeAddr>>,
) -> Self {
let mut r = Self::new(resource, nodes);
r.tag = None;
r
}
pub fn tag(mut self, tag: SetTagOption) -> Self {
self.tag = Some(tag);
self
}
pub fn progress_sender(mut self, sender: ProgressSubscriber) -> Self {
self.progress = Some(sender);
self
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, derive_more::From, derive_more::Into)]
pub struct DownloadKind(HashAndFormat);
impl DownloadKind {
pub const fn hash(&self) -> Hash {
self.0.hash
}
pub const fn format(&self) -> BlobFormat {
self.0.format
}
pub const fn hash_and_format(&self) -> HashAndFormat {
self.0
}
}
impl fmt::Display for DownloadKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{:?}", self.0.hash.fmt_short(), self.0.format)
}
}
type ExternalDownloadResult = Result<Stats, DownloadError>;
type InternalDownloadResult = Result<Stats, FailureAction>;
#[derive(Debug, Clone, thiserror::Error)]
pub enum DownloadError {
#[error("Failed to complete download")]
DownloadFailed,
#[error("Download cancelled by us")]
Cancelled,
#[error("No provider nodes found")]
NoProviders,
#[error("Failed to receive response from download service")]
ActorClosed,
}
#[derive(Debug)]
pub struct DownloadHandle {
id: IntentId,
kind: DownloadKind,
receiver: oneshot::Receiver<ExternalDownloadResult>,
}
impl std::future::Future for DownloadHandle {
type Output = ExternalDownloadResult;
fn poll(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
use std::task::Poll::*;
match std::pin::Pin::new(&mut self.receiver).poll(cx) {
Ready(Ok(result)) => Ready(result),
Ready(Err(_recv_err)) => Ready(Err(DownloadError::ActorClosed)),
Pending => Pending,
}
}
}
#[derive(Clone, Debug)]
pub struct Downloader {
next_id: Arc<AtomicU64>,
msg_tx: mpsc::Sender<Message>,
}
impl Downloader {
pub fn new<S>(store: S, endpoint: MagicEndpoint, rt: LocalPoolHandle) -> Self
where
S: Store,
{
Self::with_config(store, endpoint, rt, Default::default(), Default::default())
}
pub fn with_config<S>(
store: S,
endpoint: MagicEndpoint,
rt: LocalPoolHandle,
concurrency_limits: ConcurrencyLimits,
retry_config: RetryConfig,
) -> Self
where
S: Store,
{
let me = endpoint.node_id().fmt_short();
let (msg_tx, msg_rx) = mpsc::channel(SERVICE_CHANNEL_CAPACITY);
let dialer = iroh_net::dialer::Dialer::new(endpoint);
let create_future = move || {
let getter = get::IoGetter {
store: store.clone(),
};
let service = Service::new(
store,
getter,
dialer,
concurrency_limits,
retry_config,
msg_rx,
);
service.run().instrument(error_span!("downloader", %me))
};
rt.spawn_pinned(create_future);
Self {
next_id: Arc::new(AtomicU64::new(0)),
msg_tx,
}
}
pub async fn queue(&self, request: DownloadRequest) -> DownloadHandle {
let kind = request.kind;
let intent_id = IntentId(self.next_id.fetch_add(1, Ordering::SeqCst));
let (sender, receiver) = oneshot::channel();
let handle = DownloadHandle {
id: intent_id,
kind,
receiver,
};
let msg = Message::Queue {
on_finish: sender,
request,
intent_id,
};
if let Err(send_err) = self.msg_tx.send(msg).await {
let msg = send_err.0;
debug!(?msg, "download not sent");
}
handle
}
pub async fn cancel(&self, handle: DownloadHandle) {
let DownloadHandle {
id,
kind,
receiver: _,
} = handle;
let msg = Message::CancelIntent { id, kind };
if let Err(send_err) = self.msg_tx.send(msg).await {
let msg = send_err.0;
debug!(?msg, "cancel not sent");
}
}
pub async fn nodes_have(&mut self, hash: Hash, nodes: Vec<NodeId>) {
let msg = Message::NodesHave { hash, nodes };
if let Err(send_err) = self.msg_tx.send(msg).await {
let msg = send_err.0;
debug!(?msg, "nodes have not been sent")
}
}
}
#[derive(derive_more::Debug)]
enum Message {
Queue {
request: DownloadRequest,
#[debug(skip)]
on_finish: oneshot::Sender<ExternalDownloadResult>,
intent_id: IntentId,
},
NodesHave { hash: Hash, nodes: Vec<NodeId> },
CancelIntent { id: IntentId, kind: DownloadKind },
}
#[derive(derive_more::Debug)]
struct IntentHandlers {
#[debug("oneshot::Sender<DownloadResult>")]
on_finish: oneshot::Sender<ExternalDownloadResult>,
on_progress: Option<ProgressSubscriber>,
}
#[derive(Debug, Default)]
struct RequestInfo {
intents: HashMap<IntentId, IntentHandlers>,
tags: TagSet,
}
#[derive(derive_more::Debug)]
struct ActiveRequestInfo {
#[debug(skip)]
cancellation: CancellationToken,
node: NodeId,
temp_tag: TempTag,
}
#[derive(Debug, Default)]
struct RetryState {
retry_count: u32,
retry_is_queued: bool,
}
#[derive(derive_more::Debug)]
struct ConnectionInfo<Conn> {
#[debug(skip)]
conn: Conn,
state: ConnectedState,
}
impl<Conn> ConnectionInfo<Conn> {
fn new_idle(connection: Conn, drop_key: delay_queue::Key) -> Self {
ConnectionInfo {
conn: connection,
state: ConnectedState::Idle { drop_key },
}
}
fn active_requests(&self) -> usize {
match self.state {
ConnectedState::Busy { active_requests } => active_requests.get(),
ConnectedState::Idle { .. } => 0,
}
}
fn is_idle(&self) -> bool {
matches!(self.state, ConnectedState::Idle { .. })
}
}
#[derive(derive_more::Debug)]
enum ConnectedState {
Busy {
#[debug("{}", active_requests.get())]
active_requests: NonZeroUsize,
},
Idle {
#[debug(skip)]
drop_key: delay_queue::Key,
},
}
#[derive(Debug)]
enum NodeState<'a, Conn> {
Connected(&'a ConnectionInfo<Conn>),
Dialing,
WaitForRetry,
Disconnected,
}
#[derive(Debug)]
struct Service<G: Getter, D: Dialer, DB: Store> {
getter: G,
providers: ProviderMap,
dialer: D,
concurrency_limits: ConcurrencyLimits,
retry_config: RetryConfig,
msg_rx: mpsc::Receiver<Message>,
connected_nodes: HashMap<NodeId, ConnectionInfo<D::Connection>>,
retry_node_state: HashMap<NodeId, RetryState>,
retry_nodes_queue: delay_queue::DelayQueue<NodeId>,
goodbye_nodes_queue: delay_queue::DelayQueue<NodeId>,
queue: Queue,
requests: HashMap<DownloadKind, RequestInfo>,
active_requests: HashMap<DownloadKind, ActiveRequestInfo>,
in_progress_downloads: JoinSet<(DownloadKind, InternalDownloadResult)>,
progress_tracker: ProgressTracker,
db: DB,
}
impl<DB: Store, G: Getter<Connection = D::Connection>, D: Dialer> Service<G, D, DB> {
fn new(
db: DB,
getter: G,
dialer: D,
concurrency_limits: ConcurrencyLimits,
retry_config: RetryConfig,
msg_rx: mpsc::Receiver<Message>,
) -> Self {
Service {
getter,
dialer,
msg_rx,
concurrency_limits,
retry_config,
connected_nodes: Default::default(),
retry_node_state: Default::default(),
providers: Default::default(),
requests: Default::default(),
retry_nodes_queue: delay_queue::DelayQueue::default(),
goodbye_nodes_queue: delay_queue::DelayQueue::default(),
active_requests: Default::default(),
in_progress_downloads: Default::default(),
progress_tracker: ProgressTracker::new(),
queue: Default::default(),
db,
}
}
async fn run(mut self) {
loop {
trace!("wait for tick");
tokio::select! {
Some((node, conn_result)) = self.dialer.next() => {
trace!(node=%node.fmt_short(), "tick: connection ready");
self.on_connection_ready(node, conn_result);
}
maybe_msg = self.msg_rx.recv() => {
trace!(msg=?maybe_msg, "tick: message received");
match maybe_msg {
Some(msg) => self.handle_message(msg).await,
None => return self.shutdown().await,
}
}
Some(res) = self.in_progress_downloads.join_next(), if !self.in_progress_downloads.is_empty() => {
match res {
Ok((kind, result)) => {
trace!(%kind, "tick: transfer completed");
self.on_download_completed(kind, result).await;
}
Err(err) => {
warn!(?err, "transfer task panicked");
}
}
}
Some(expired) = self.retry_nodes_queue.next() => {
let node = expired.into_inner();
trace!(node=%node.fmt_short(), "tick: retry node");
self.on_retry_wait_elapsed(node);
}
Some(expired) = self.goodbye_nodes_queue.next() => {
let node = expired.into_inner();
trace!(node=%node.fmt_short(), "tick: goodbye node");
self.disconnect_idle_node(node, "idle expired");
}
}
self.process_head();
#[cfg(any(test, debug_assertions))]
self.check_invariants();
}
}
async fn handle_message(&mut self, msg: Message) {
match msg {
Message::Queue {
request,
on_finish,
intent_id,
} => {
self.handle_queue_new_download(request, intent_id, on_finish)
.await
}
Message::CancelIntent { id, kind } => self.handle_cancel_download(id, kind).await,
Message::NodesHave { hash, nodes } => {
let updated = self
.providers
.add_nodes_if_hash_exists(hash, nodes.iter().cloned());
if updated {
self.queue.unpark_hash(hash);
}
}
}
}
async fn handle_queue_new_download(
&mut self,
request: DownloadRequest,
intent_id: IntentId,
on_finish: oneshot::Sender<ExternalDownloadResult>,
) {
let DownloadRequest {
kind,
nodes,
tag,
progress,
} = request;
debug!(%kind, nodes=?nodes.iter().map(|n| n.node_id.fmt_short()).collect::<Vec<_>>(), "queue intent");
let intent_handlers = IntentHandlers {
on_finish,
on_progress: progress,
};
if nodes.is_empty() && self.providers.get_candidates(&kind.hash()).next().is_none() {
self.finalize_download(
kind,
[(intent_id, intent_handlers)].into(),
Err(DownloadError::NoProviders),
);
return;
}
let updated = self
.providers
.add_hash_with_nodes(kind.hash(), nodes.iter().map(|n| n.node_id));
if self.active_requests.contains_key(&kind) {
if let Some(on_progress) = &intent_handlers.on_progress {
if let Err(err) = self
.progress_tracker
.subscribe(kind, on_progress.clone())
.await
{
debug!(?err, %kind, "failed to subscribe progress sender to transfer");
}
}
} else {
if updated && self.queue.is_parked(&kind) {
self.queue.unpark(&kind);
} else if !self.queue.contains(&kind) {
self.queue.insert(kind);
}
}
let request_info = self.requests.entry(kind).or_default();
request_info.intents.insert(intent_id, intent_handlers);
if let Some(tag) = &tag {
request_info.tags.insert(tag.clone());
}
}
async fn handle_cancel_download(&mut self, intent_id: IntentId, kind: DownloadKind) {
let Entry::Occupied(mut occupied_entry) = self.requests.entry(kind) else {
warn!(%kind, %intent_id, "cancel download called for unknown download");
return;
};
let request_info = occupied_entry.get_mut();
if let Some(handlers) = request_info.intents.remove(&intent_id) {
handlers.on_finish.send(Err(DownloadError::Cancelled)).ok();
if let Some(sender) = handlers.on_progress {
self.progress_tracker.unsubscribe(&kind, &sender);
sender
.send(DownloadProgress::Abort(
anyhow::Error::from(DownloadError::Cancelled).into(),
))
.await
.ok();
}
}
if request_info.intents.is_empty() {
occupied_entry.remove();
if let Entry::Occupied(occupied_entry) = self.active_requests.entry(kind) {
occupied_entry.remove().cancellation.cancel();
} else {
self.queue.remove(&kind);
}
self.remove_hash_if_not_queued(&kind.hash());
}
}
fn on_connection_ready(&mut self, node: NodeId, result: anyhow::Result<D::Connection>) {
debug_assert!(
!self.connected_nodes.contains_key(&node),
"newly connected node is not yet connected"
);
match result {
Ok(connection) => {
trace!(node=%node.fmt_short(), "connected to node");
let drop_key = self.goodbye_nodes_queue.insert(node, IDLE_PEER_TIMEOUT);
self.connected_nodes
.insert(node, ConnectionInfo::new_idle(connection, drop_key));
}
Err(err) => {
debug!(%node, %err, "connection to node failed");
self.disconnect_and_retry(node);
}
}
}
async fn on_download_completed(&mut self, kind: DownloadKind, result: InternalDownloadResult) {
let active_request_info = self
.active_requests
.remove(&kind)
.expect("request was active");
let request_info = self.requests.remove(&kind).expect("request was active");
let ActiveRequestInfo { node, temp_tag, .. } = active_request_info;
let node_info = self
.connected_nodes
.get_mut(&node)
.expect("node exists in the mapping");
node_info.state = match NonZeroUsize::new(node_info.active_requests() - 1) {
None => {
let drop_key = self.goodbye_nodes_queue.insert(node, IDLE_PEER_TIMEOUT);
ConnectedState::Idle { drop_key }
}
Some(active_requests) => ConnectedState::Busy { active_requests },
};
match &result {
Ok(_) => {
debug!(%kind, node=%node.fmt_short(), "download successful");
self.retry_node_state.remove(&node);
}
Err(FailureAction::AllIntentsDropped) => {
debug!(%kind, node=%node.fmt_short(), "download cancelled");
}
Err(FailureAction::AbortRequest(reason)) => {
debug!(%kind, node=%node.fmt_short(), %reason, "download failed: abort request");
self.providers.remove_hash_from_node(&kind.hash(), &node);
}
Err(FailureAction::DropPeer(reason)) => {
debug!(%kind, node=%node.fmt_short(), %reason, "download failed: drop node");
if node_info.is_idle() {
self.remove_node(node, "explicit drop");
} else {
self.providers.remove_hash_from_node(&kind.hash(), &node);
}
}
Err(FailureAction::RetryLater(reason)) => {
debug!(%kind, node=%node.fmt_short(), %reason, "download failed: retry later");
if node_info.is_idle() {
self.disconnect_and_retry(node);
}
}
};
let finalize = match &result {
Ok(_) | Err(FailureAction::AllIntentsDropped) => true,
_ => !self.providers.has_candidates(&kind.hash()),
};
if finalize {
let result = result.map_err(|_| DownloadError::DownloadFailed);
if result.is_ok() {
request_info.tags.apply(&self.db, kind.0).await.ok();
}
drop(temp_tag);
self.finalize_download(kind, request_info.intents, result);
} else {
self.requests.insert(kind, request_info);
self.queue.insert_front(kind);
}
}
fn finalize_download(
&mut self,
kind: DownloadKind,
intents: HashMap<IntentId, IntentHandlers>,
result: ExternalDownloadResult,
) {
self.progress_tracker.remove(&kind);
self.remove_hash_if_not_queued(&kind.hash());
let result = result.map_err(|_| DownloadError::DownloadFailed);
for (_id, handlers) in intents.into_iter() {
handlers.on_finish.send(result.clone()).ok();
}
}
fn on_retry_wait_elapsed(&mut self, node: NodeId) {
let Some(hashes) = self.providers.node_hash.get(&node) else {
self.retry_node_state.remove(&node);
return;
};
let Some(state) = self.retry_node_state.get_mut(&node) else {
warn!(node=%node.fmt_short(), "missing retry state for node ready for retry");
return;
};
state.retry_is_queued = false;
for hash in hashes {
self.queue.unpark_hash(*hash);
}
}
fn process_head(&mut self) {
loop {
let Some(kind) = self.queue.front().cloned() else {
break;
};
let next_step = self.next_step(&kind);
trace!(%kind, ?next_step, "process_head");
match next_step {
NextStep::Wait => break,
NextStep::StartTransfer(node) => {
let _ = self.queue.pop_front();
debug!(%kind, node=%node.fmt_short(), "start transfer");
self.start_download(kind, node);
}
NextStep::Dial(node) => {
debug!(%kind, node=%node.fmt_short(), "dial node");
self.dialer.queue_dial(node);
}
NextStep::DialQueuedDisconnect(node, key) => {
let idle_node = self.goodbye_nodes_queue.remove(&key).into_inner();
self.disconnect_idle_node(idle_node, "drop idle for new dial");
debug!(%kind, node=%node.fmt_short(), idle_node=%idle_node.fmt_short(), "dial node, disconnect idle node)");
self.dialer.queue_dial(node);
}
NextStep::Park => {
debug!(%kind, "park download: all providers waiting for retry");
self.queue.park_front();
}
NextStep::OutOfProviders => {
debug!(%kind, "abort download: out of providers");
let _ = self.queue.pop_front();
let info = self.requests.remove(&kind).expect("queued downloads exist");
self.finalize_download(kind, info.intents, Err(DownloadError::NoProviders));
}
}
}
}
fn disconnect_and_retry(&mut self, node: NodeId) {
self.disconnect_idle_node(node, "queue retry");
let retry_state = self.retry_node_state.entry(node).or_default();
retry_state.retry_count += 1;
if retry_state.retry_count <= self.retry_config.max_retries_per_node {
debug!(node=%node.fmt_short(), retry_count=retry_state.retry_count, "queue retry");
let timeout = self.retry_config.initial_retry_delay * retry_state.retry_count;
self.retry_nodes_queue.insert(node, timeout);
retry_state.retry_is_queued = true;
} else {
self.remove_node(node, "retries exceeded");
}
}
fn next_step(&self, kind: &DownloadKind) -> NextStep {
if self
.concurrency_limits
.at_requests_capacity(self.active_requests.len())
{
return NextStep::Wait;
};
let mut candidates = self.providers.get_candidates(&kind.hash()).peekable();
if candidates.peek().is_none() {
return NextStep::OutOfProviders;
}
let mut best_connected: Option<(NodeId, usize)> = None;
let mut next_to_dial = None;
let mut currently_dialing = 0;
let mut has_exhausted_provider = false;
let mut has_retrying_provider = false;
for node in candidates {
match self.node_state(node) {
NodeState::Connected(info) => {
let active_requests = info.active_requests();
if self
.concurrency_limits
.node_at_request_capacity(active_requests)
{
has_exhausted_provider = true;
} else {
best_connected = Some(match best_connected.take() {
Some(old) if old.1 <= active_requests => old,
_ => (*node, active_requests),
});
}
}
NodeState::Dialing => {
currently_dialing += 1;
}
NodeState::WaitForRetry => {
has_retrying_provider = true;
}
NodeState::Disconnected => {
if next_to_dial.is_none() {
next_to_dial = Some(node);
}
}
}
}
let has_dialing = currently_dialing > 0;
if let Some((node, _active_requests)) = best_connected {
NextStep::StartTransfer(node)
}
else if let Some(node) = next_to_dial {
let at_dial_capacity = has_dialing
&& self
.concurrency_limits
.at_dials_per_hash_capacity(currently_dialing);
let at_connections_capacity = self.at_connections_capacity();
if !at_connections_capacity && !at_dial_capacity {
NextStep::Dial(*node)
}
else if at_connections_capacity
&& !at_dial_capacity
&& !self.goodbye_nodes_queue.is_empty()
{
let key = self.goodbye_nodes_queue.peek().expect("just checked");
NextStep::DialQueuedDisconnect(*node, key)
}
else {
NextStep::Wait
}
}
else if has_exhausted_provider || has_dialing {
NextStep::Wait
}
else if has_retrying_provider {
NextStep::Park
}
else {
NextStep::OutOfProviders
}
}
fn start_download(&mut self, kind: DownloadKind, node: NodeId) {
let node_info = self.connected_nodes.get_mut(&node).expect("node exists");
let request_info = self.requests.get(&kind).expect("hash exists");
let subscribers = request_info
.intents
.values()
.flat_map(|state| state.on_progress.clone());
let progress_sender = self.progress_tracker.track(kind, subscribers);
let cancellation = CancellationToken::new();
let temp_tag = self.db.temp_tag(kind.0);
let state = ActiveRequestInfo {
cancellation: cancellation.clone(),
node,
temp_tag,
};
let conn = node_info.conn.clone();
let get_fut = self.getter.get(kind, conn, progress_sender);
let fut = async move {
let res = tokio::select! {
_ = cancellation.cancelled() => Err(FailureAction::AllIntentsDropped),
res = get_fut => res
};
trace!("transfer finished");
(kind, res)
}
.instrument(error_span!("transfer", %kind, node=%node.fmt_short()));
node_info.state = match &node_info.state {
ConnectedState::Busy { active_requests } => ConnectedState::Busy {
active_requests: active_requests.saturating_add(1),
},
ConnectedState::Idle { drop_key } => {
self.goodbye_nodes_queue.remove(drop_key);
ConnectedState::Busy {
active_requests: NonZeroUsize::new(1).expect("clearly non zero"),
}
}
};
self.active_requests.insert(kind, state);
self.in_progress_downloads.spawn_local(fut);
}
fn disconnect_idle_node(&mut self, node: NodeId, reason: &'static str) -> bool {
if let Some(info) = self.connected_nodes.remove(&node) {
match info.state {
ConnectedState::Idle { drop_key } => {
self.goodbye_nodes_queue.try_remove(&drop_key);
true
}
ConnectedState::Busy { .. } => {
warn!("expected removed node to be idle, but is busy (removal reason: {reason:?})");
self.connected_nodes.insert(node, info);
false
}
}
} else {
true
}
}
fn remove_node(&mut self, node: NodeId, reason: &'static str) {
debug!(node = %node.fmt_short(), %reason, "remove node");
if self.disconnect_idle_node(node, reason) {
self.providers.remove_node(&node);
self.retry_node_state.remove(&node);
}
}
fn node_state<'a>(&'a self, node: &NodeId) -> NodeState<'a, D::Connection> {
if let Some(info) = self.connected_nodes.get(node) {
NodeState::Connected(info)
} else if self.dialer.is_pending(node) {
NodeState::Dialing
} else {
match self.retry_node_state.get(node) {
Some(state) if state.retry_is_queued => NodeState::WaitForRetry,
_ => NodeState::Disconnected,
}
}
}
fn at_connections_capacity(&self) -> bool {
self.concurrency_limits
.at_connections_capacity(self.connections_count())
}
fn connections_count(&self) -> usize {
let connected_nodes = self.connected_nodes.values().count();
let dialing_nodes = self.dialer.pending_count();
connected_nodes + dialing_nodes
}
fn remove_hash_if_not_queued(&mut self, hash: &Hash) {
if !self.queue.contains_hash(*hash) {
self.providers.remove_hash(hash);
}
}
#[allow(clippy::unused_async)]
async fn shutdown(self) {
debug!("shutting down");
}
}
#[derive(Debug)]
enum NextStep {
StartTransfer(NodeId),
Dial(NodeId),
DialQueuedDisconnect(NodeId, delay_queue::Key),
Wait,
Park,
OutOfProviders,
}
#[derive(Default, Debug)]
struct ProviderMap {
hash_node: HashMap<Hash, HashSet<NodeId>>,
node_hash: HashMap<NodeId, HashSet<Hash>>,
}
impl ProviderMap {
pub fn get_candidates(&self, hash: &Hash) -> impl Iterator<Item = &NodeId> {
self.hash_node
.get(hash)
.map(|nodes| nodes.iter())
.into_iter()
.flatten()
}
pub fn has_candidates(&self, hash: &Hash) -> bool {
self.hash_node
.get(hash)
.map(|nodes| !nodes.is_empty())
.unwrap_or(false)
}
fn add_hash_with_nodes(&mut self, hash: Hash, nodes: impl Iterator<Item = NodeId>) -> bool {
let mut updated = false;
let hash_entry = self.hash_node.entry(hash).or_default();
for node in nodes {
updated |= hash_entry.insert(node);
let node_entry = self.node_hash.entry(node).or_default();
node_entry.insert(hash);
}
updated
}
fn add_nodes_if_hash_exists(
&mut self,
hash: Hash,
nodes: impl Iterator<Item = NodeId>,
) -> bool {
let mut updated = false;
if let Some(hash_entry) = self.hash_node.get_mut(&hash) {
for node in nodes {
updated |= hash_entry.insert(node);
let node_entry = self.node_hash.entry(node).or_default();
node_entry.insert(hash);
}
}
updated
}
fn remove_hash(&mut self, hash: &Hash) {
if let Some(nodes) = self.hash_node.remove(hash) {
for node in nodes {
if let Some(hashes) = self.node_hash.get_mut(&node) {
hashes.remove(hash);
if hashes.is_empty() {
self.node_hash.remove(&node);
}
}
}
}
}
fn remove_node(&mut self, node: &NodeId) {
if let Some(hashes) = self.node_hash.remove(node) {
for hash in hashes {
if let Some(nodes) = self.hash_node.get_mut(&hash) {
nodes.remove(node);
if nodes.is_empty() {
self.hash_node.remove(&hash);
}
}
}
}
}
fn remove_hash_from_node(&mut self, hash: &Hash, node: &NodeId) {
if let Some(nodes) = self.hash_node.get_mut(hash) {
nodes.remove(node);
if nodes.is_empty() {
self.remove_hash(hash);
}
}
if let Some(hashes) = self.node_hash.get_mut(node) {
hashes.remove(hash);
if hashes.is_empty() {
self.remove_node(node);
}
}
}
}
#[derive(Debug, Default)]
struct Queue {
main: LinkedHashSet<DownloadKind>,
parked: HashSet<DownloadKind>,
}
impl Queue {
pub fn front(&self) -> Option<&DownloadKind> {
self.main.front()
}
#[cfg(any(test, debug_assertions))]
pub fn iter_parked(&self) -> impl Iterator<Item = &DownloadKind> {
self.parked.iter()
}
#[cfg(any(test, debug_assertions))]
pub fn iter(&self) -> impl Iterator<Item = &DownloadKind> {
self.main.iter().chain(self.parked.iter())
}
pub fn contains(&self, kind: &DownloadKind) -> bool {
self.main.contains(kind) || self.parked.contains(kind)
}
pub fn contains_hash(&self, hash: Hash) -> bool {
let as_raw = HashAndFormat::raw(hash).into();
let as_hash_seq = HashAndFormat::hash_seq(hash).into();
self.contains(&as_raw) || self.contains(&as_hash_seq)
}
pub fn is_parked(&self, kind: &DownloadKind) -> bool {
self.parked.contains(kind)
}
pub fn insert(&mut self, kind: DownloadKind) {
if !self.main.contains(&kind) {
self.main.insert(kind);
}
}
pub fn insert_front(&mut self, kind: DownloadKind) {
if !self.main.contains(&kind) {
self.main.insert(kind);
}
self.main.to_front(&kind);
}
pub fn pop_front(&mut self) -> Option<DownloadKind> {
self.main.pop_front()
}
pub fn park_front(&mut self) {
if let Some(item) = self.pop_front() {
self.parked.insert(item);
}
}
pub fn unpark(&mut self, kind: &DownloadKind) {
if self.parked.remove(kind) {
self.main.insert(*kind);
self.main.to_front(kind);
}
}
pub fn unpark_hash(&mut self, hash: Hash) {
let as_raw = HashAndFormat::raw(hash).into();
let as_hash_seq = HashAndFormat::hash_seq(hash).into();
self.unpark(&as_raw);
self.unpark(&as_hash_seq);
}
pub fn remove(&mut self, kind: &DownloadKind) -> bool {
self.main.remove(kind) || self.parked.remove(kind)
}
}
impl Dialer for iroh_net::dialer::Dialer {
type Connection = quinn::Connection;
fn queue_dial(&mut self, node_id: NodeId) {
self.queue_dial(node_id, crate::protocol::ALPN)
}
fn pending_count(&self) -> usize {
self.pending_count()
}
fn is_pending(&self, node: &NodeId) -> bool {
self.is_pending(node)
}
}