fixed search
This commit is contained in:
@@ -128,8 +128,16 @@ max records per FindValue response = 100
|
|||||||
max artist name = 512 bytes, max tokens = 32
|
max artist name = 512 bytes, max tokens = 32
|
||||||
max pending requests = 1024
|
max pending requests = 1024
|
||||||
request timeout = 5 s, lookup timeout = 15 s
|
request timeout = 5 s, lookup timeout = 15 s
|
||||||
|
on-demand dial timeout = 5 s
|
||||||
|
dial backoff = 30 s doubling up to 10 min, eviction after 5 failures
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Value lookups return as soon as the first records arrive; in-flight requests
|
||||||
|
to slower or dead contacts are cancelled. Contacts that failed to dial are
|
||||||
|
skipped for an exponentially growing backoff window and evicted after five
|
||||||
|
consecutive failures (peer exchange re-adds them with a clean slate if they
|
||||||
|
come back).
|
||||||
|
|
||||||
## Library usage
|
## Library usage
|
||||||
|
|
||||||
The CLI is a thin wrapper around the `artist-dht` library:
|
The CLI is a thin wrapper around the `artist-dht` library:
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
pub const DEFAULT_LOOKUP_TIMEOUT: Duration = Duration::from_secs(15);
|
pub const DEFAULT_LOOKUP_TIMEOUT: Duration = Duration::from_secs(15);
|
||||||
/// Default timeout for transport operations (dialing, handshakes, sends).
|
/// Default timeout for transport operations (dialing, handshakes, sends).
|
||||||
pub const DEFAULT_TRANSPORT_TIMEOUT: Duration = Duration::from_secs(15);
|
pub const DEFAULT_TRANSPORT_TIMEOUT: Duration = Duration::from_secs(15);
|
||||||
|
/// Default timeout for on-demand dials during DHT operations.
|
||||||
|
pub const DEFAULT_DIAL_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
/// Configuration for an [`crate::ArtistDhtService`].
|
/// Configuration for an [`crate::ArtistDhtService`].
|
||||||
///
|
///
|
||||||
@@ -40,6 +42,12 @@ pub struct ArtistDhtConfig {
|
|||||||
/// establishing a connection through relays can take much longer than a
|
/// establishing a connection through relays can take much longer than a
|
||||||
/// request over an existing one.
|
/// request over an existing one.
|
||||||
pub transport_timeout: Duration,
|
pub transport_timeout: Duration,
|
||||||
|
/// Timeout for dialing a contact **on demand during DHT operations**
|
||||||
|
/// (lookups, publishes). Deliberately shorter than `transport_timeout`:
|
||||||
|
/// a dead contact must not stall a whole lookup, and a peer that needs
|
||||||
|
/// longer than this to dial will still be reached by the periodic
|
||||||
|
/// rendezvous/republish machinery.
|
||||||
|
pub dial_timeout: Duration,
|
||||||
/// Automatic peer discovery over the mainline DHT: peers of the same
|
/// Automatic peer discovery over the mainline DHT: peers of the same
|
||||||
/// network find each other knowing nothing but the network id. `None`
|
/// network find each other knowing nothing but the network id. `None`
|
||||||
/// disables it; peers are then connected via tickets only.
|
/// disables it; peers are then connected via tickets only.
|
||||||
@@ -66,6 +74,7 @@ pub struct ArtistDhtConfigBuilder {
|
|||||||
request_timeout: Option<Duration>,
|
request_timeout: Option<Duration>,
|
||||||
lookup_timeout: Option<Duration>,
|
lookup_timeout: Option<Duration>,
|
||||||
transport_timeout: Option<Duration>,
|
transport_timeout: Option<Duration>,
|
||||||
|
dial_timeout: Option<Duration>,
|
||||||
rendezvous: Option<RendezvousConfig>,
|
rendezvous: Option<RendezvousConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +121,12 @@ impl ArtistDhtConfigBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the timeout for on-demand dials during DHT operations.
|
||||||
|
pub fn dial_timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.dial_timeout = Some(timeout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Enables automatic peer discovery over the mainline DHT.
|
/// Enables automatic peer discovery over the mainline DHT.
|
||||||
pub fn rendezvous(mut self, rendezvous: RendezvousConfig) -> Self {
|
pub fn rendezvous(mut self, rendezvous: RendezvousConfig) -> Self {
|
||||||
self.rendezvous = Some(rendezvous);
|
self.rendezvous = Some(rendezvous);
|
||||||
@@ -142,6 +157,7 @@ impl ArtistDhtConfigBuilder {
|
|||||||
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
|
request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
|
||||||
lookup_timeout: self.lookup_timeout.unwrap_or(DEFAULT_LOOKUP_TIMEOUT),
|
lookup_timeout: self.lookup_timeout.unwrap_or(DEFAULT_LOOKUP_TIMEOUT),
|
||||||
transport_timeout: self.transport_timeout.unwrap_or(DEFAULT_TRANSPORT_TIMEOUT),
|
transport_timeout: self.transport_timeout.unwrap_or(DEFAULT_TRANSPORT_TIMEOUT),
|
||||||
|
dial_timeout: self.dial_timeout.unwrap_or(DEFAULT_DIAL_TIMEOUT),
|
||||||
rendezvous: self.rendezvous,
|
rendezvous: self.rendezvous,
|
||||||
};
|
};
|
||||||
for (name, value) in [
|
for (name, value) in [
|
||||||
@@ -150,6 +166,7 @@ impl ArtistDhtConfigBuilder {
|
|||||||
("request_timeout", config.request_timeout),
|
("request_timeout", config.request_timeout),
|
||||||
("lookup_timeout", config.lookup_timeout),
|
("lookup_timeout", config.lookup_timeout),
|
||||||
("transport_timeout", config.transport_timeout),
|
("transport_timeout", config.transport_timeout),
|
||||||
|
("dial_timeout", config.dial_timeout),
|
||||||
] {
|
] {
|
||||||
if value.is_zero() {
|
if value.is_zero() {
|
||||||
return Err(ArtistDhtError::Database(format!(
|
return Err(ArtistDhtError::Database(format!(
|
||||||
|
|||||||
@@ -327,6 +327,18 @@ impl Database {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deletes a persisted peer contact (e.g. after repeated failed dials).
|
||||||
|
pub async fn delete_known_peer(&self, peer_id: EndpointId) -> Result<()> {
|
||||||
|
self.call(move |conn| {
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM known_peers WHERE peer_id = ?1",
|
||||||
|
params![peer_id.to_string()],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Loads all persisted peer contacts.
|
/// Loads all persisted peer contacts.
|
||||||
pub async fn load_known_peers(&self) -> Result<Vec<NodeContact>> {
|
pub async fn load_known_peers(&self) -> Result<Vec<NodeContact>> {
|
||||||
self.call(|conn| {
|
self.call(|conn| {
|
||||||
|
|||||||
+156
-36
@@ -4,10 +4,11 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
|
||||||
use std::time::Instant;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use federation_net::{EndpointId, NetworkEngine, NetworkEvent, NetworkEventReceiver, PeerTicket};
|
use federation_net::{EndpointId, NetworkEngine, NetworkEvent, NetworkEventReceiver, PeerTicket};
|
||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
|
use futures::stream::{FuturesUnordered, StreamExt};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
@@ -30,6 +31,29 @@ fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
|
|||||||
mutex.lock().unwrap_or_else(PoisonError::into_inner)
|
mutex.lock().unwrap_or_else(PoisonError::into_inner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Base of the exponential backoff applied to a contact after a failed dial.
|
||||||
|
const DIAL_BACKOFF_BASE: Duration = Duration::from_secs(30);
|
||||||
|
/// Upper bound of the dial backoff.
|
||||||
|
const DIAL_BACKOFF_MAX: Duration = Duration::from_secs(10 * 60);
|
||||||
|
/// Consecutive failed dials after which a contact is evicted entirely.
|
||||||
|
const DIAL_FAILURES_BEFORE_EVICT: u32 = 5;
|
||||||
|
|
||||||
|
/// Dial-failure state of one currently unreachable contact.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct DialFailure {
|
||||||
|
consecutive: u32,
|
||||||
|
last_attempt_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How long a contact is skipped after `consecutive` failed dials:
|
||||||
|
/// 30s, 1m, 2m, 4m, 8m, then capped at 10 minutes.
|
||||||
|
fn dial_backoff(consecutive: u32) -> Duration {
|
||||||
|
let exponent = consecutive.saturating_sub(1).min(8);
|
||||||
|
DIAL_BACKOFF_BASE
|
||||||
|
.saturating_mul(1u32 << exponent)
|
||||||
|
.min(DIAL_BACKOFF_MAX)
|
||||||
|
}
|
||||||
|
|
||||||
/// An outbound DHT request, before it is wrapped in an envelope.
|
/// An outbound DHT request, before it is wrapped in an envelope.
|
||||||
enum OutboundRequest {
|
enum OutboundRequest {
|
||||||
Ping,
|
Ping,
|
||||||
@@ -63,6 +87,8 @@ pub(crate) struct Node {
|
|||||||
hello_sent: Mutex<HashSet<EndpointId>>,
|
hello_sent: Mutex<HashSet<EndpointId>>,
|
||||||
/// Peers we already gossiped contacts to (per connection).
|
/// Peers we already gossiped contacts to (per connection).
|
||||||
exchange_sent: Mutex<HashSet<EndpointId>>,
|
exchange_sent: Mutex<HashSet<EndpointId>>,
|
||||||
|
/// Contacts that recently failed to dial, with their backoff state.
|
||||||
|
dial_failures: Mutex<HashMap<EndpointId, DialFailure>>,
|
||||||
events: Mutex<Option<mpsc::Sender<ArtistDhtEvent>>>,
|
events: Mutex<Option<mpsc::Sender<ArtistDhtEvent>>>,
|
||||||
/// Set once the post-startup republish has been triggered.
|
/// Set once the post-startup republish has been triggered.
|
||||||
initial_republish_done: AtomicBool,
|
initial_republish_done: AtomicBool,
|
||||||
@@ -88,6 +114,7 @@ impl Node {
|
|||||||
pending: PendingRequests::default(),
|
pending: PendingRequests::default(),
|
||||||
hello_sent: Mutex::new(HashSet::new()),
|
hello_sent: Mutex::new(HashSet::new()),
|
||||||
exchange_sent: Mutex::new(HashSet::new()),
|
exchange_sent: Mutex::new(HashSet::new()),
|
||||||
|
dial_failures: Mutex::new(HashMap::new()),
|
||||||
events: Mutex::new(Some(events)),
|
events: Mutex::new(Some(events)),
|
||||||
initial_republish_done: AtomicBool::new(false),
|
initial_republish_done: AtomicBool::new(false),
|
||||||
shutting_down: AtomicBool::new(false),
|
shutting_down: AtomicBool::new(false),
|
||||||
@@ -193,6 +220,7 @@ impl Node {
|
|||||||
match event {
|
match event {
|
||||||
NetworkEvent::PeerConnected { peer_id, .. } => {
|
NetworkEvent::PeerConnected { peer_id, .. } => {
|
||||||
debug!(peer = %peer_id, "peer connected");
|
debug!(peer = %peer_id, "peer connected");
|
||||||
|
self.clear_dial_failures(&peer_id);
|
||||||
self.emit(ArtistDhtEvent::PeerConnected { peer_id }).await;
|
self.emit(ArtistDhtEvent::PeerConnected { peer_id }).await;
|
||||||
self.send_hello(peer_id).await;
|
self.send_hello(peer_id).await;
|
||||||
}
|
}
|
||||||
@@ -427,6 +455,9 @@ impl Node {
|
|||||||
|
|
||||||
/// Makes sure a connection to the contact exists, dialing its ticket if
|
/// Makes sure a connection to the contact exists, dialing its ticket if
|
||||||
/// necessary. The Hello exchange runs asynchronously via the event loop.
|
/// necessary. The Hello exchange runs asynchronously via the event loop.
|
||||||
|
///
|
||||||
|
/// On-demand dials are bounded by the (short) `dial_timeout` and feed the
|
||||||
|
/// dial-failure backoff, so unreachable contacts cannot stall lookups.
|
||||||
async fn ensure_connected(&self, contact: &NodeContact) -> Result<EndpointId> {
|
async fn ensure_connected(&self, contact: &NodeContact) -> Result<EndpointId> {
|
||||||
self.ensure_running()?;
|
self.ensure_running()?;
|
||||||
if self.engine.is_connected(contact.peer_id) {
|
if self.engine.is_connected(contact.peer_id) {
|
||||||
@@ -437,8 +468,71 @@ impl Node {
|
|||||||
.parse()
|
.parse()
|
||||||
.map_err(|err| ArtistDhtError::InvalidTicket(format!("{err}")))?;
|
.map_err(|err| ArtistDhtError::InvalidTicket(format!("{err}")))?;
|
||||||
debug!(peer = %contact.peer_id, "connecting on demand");
|
debug!(peer = %contact.peer_id, "connecting on demand");
|
||||||
let peer = self.engine.connect(ticket).await?;
|
let result = timeout(self.config.dial_timeout, self.engine.connect(ticket))
|
||||||
Ok(peer)
|
.await
|
||||||
|
.map_err(|_| ArtistDhtError::Timeout)
|
||||||
|
.and_then(|res| res.map_err(Into::into));
|
||||||
|
match result {
|
||||||
|
Ok(peer) => {
|
||||||
|
self.clear_dial_failures(&peer);
|
||||||
|
Ok(peer)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.note_dial_failure(contact).await;
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forgets the dial-failure history of a peer (it proved reachable).
|
||||||
|
fn clear_dial_failures(&self, peer: &EndpointId) {
|
||||||
|
lock(&self.dial_failures).remove(peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true` if the contact recently failed to dial and its backoff window
|
||||||
|
/// has not elapsed yet. Connected peers are never considered backed off.
|
||||||
|
fn dial_backoff_active(&self, peer: &EndpointId, now_ms: u64) -> bool {
|
||||||
|
if self.engine.is_connected(*peer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match lock(&self.dial_failures).get(peer) {
|
||||||
|
Some(failure) => {
|
||||||
|
let backoff = dial_backoff(failure.consecutive).as_millis() as u64;
|
||||||
|
now_ms < failure.last_attempt_ms.saturating_add(backoff)
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records a failed dial; after [`DIAL_FAILURES_BEFORE_EVICT`] failures
|
||||||
|
/// in a row the contact is dropped from the routing table and the
|
||||||
|
/// database (gossip re-adds it with a clean slate if it comes back).
|
||||||
|
async fn note_dial_failure(&self, contact: &NodeContact) {
|
||||||
|
let consecutive = {
|
||||||
|
let mut failures = lock(&self.dial_failures);
|
||||||
|
let failure = failures.entry(contact.peer_id).or_insert(DialFailure {
|
||||||
|
consecutive: 0,
|
||||||
|
last_attempt_ms: 0,
|
||||||
|
});
|
||||||
|
failure.consecutive += 1;
|
||||||
|
failure.last_attempt_ms = now_ms();
|
||||||
|
failure.consecutive
|
||||||
|
};
|
||||||
|
if consecutive < DIAL_FAILURES_BEFORE_EVICT {
|
||||||
|
debug!(
|
||||||
|
peer = %contact.peer_id,
|
||||||
|
consecutive,
|
||||||
|
backoff_s = dial_backoff(consecutive).as_secs(),
|
||||||
|
"dial failed; backing off"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lock(&self.dial_failures).remove(&contact.peer_id);
|
||||||
|
lock(&self.routing).remove(&contact.peer_id);
|
||||||
|
if let Err(err) = self.db.delete_known_peer(contact.peer_id).await {
|
||||||
|
warn!(error = %err, "failed to delete evicted peer from the database");
|
||||||
|
}
|
||||||
|
info!(peer = %contact.peer_id, "evicted unreachable DHT contact");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends one request and awaits its response, cleaning up the pending
|
/// Sends one request and awaits its response, cleaning up the pending
|
||||||
@@ -451,6 +545,9 @@ impl Node {
|
|||||||
let peer = self.ensure_connected(contact).await?;
|
let peer = self.ensure_connected(contact).await?;
|
||||||
let request_id = RequestId::random();
|
let request_id = RequestId::random();
|
||||||
let receiver = self.pending.register(request_id, peer)?;
|
let receiver = self.pending.register(request_id, peer)?;
|
||||||
|
// Lookups may cancel this future (early exit); the guard makes sure
|
||||||
|
// the pending entry never outlives it.
|
||||||
|
let _cleanup = self.pending.remove_on_drop(request_id);
|
||||||
tracing::trace!(pending = self.pending.len(), peer = %peer, "sending DHT request");
|
tracing::trace!(pending = self.pending.len(), peer = %peer, "sending DHT request");
|
||||||
let message = match request {
|
let message = match request {
|
||||||
OutboundRequest::Ping => ArtistDhtMessage::Ping(RequestEnvelope {
|
OutboundRequest::Ping => ArtistDhtMessage::Ping(RequestEnvelope {
|
||||||
@@ -470,23 +567,14 @@ impl Node {
|
|||||||
payload,
|
payload,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
if let Err(err) = self.send_message(peer, &message).await {
|
self.send_message(peer, &message).await?;
|
||||||
self.pending.remove(&request_id);
|
|
||||||
return Err(err);
|
|
||||||
}
|
|
||||||
match timeout(self.config.request_timeout, receiver).await {
|
match timeout(self.config.request_timeout, receiver).await {
|
||||||
Ok(Ok(response)) => {
|
Ok(Ok(response)) => {
|
||||||
lock(&self.routing).touch(&peer, now_ms());
|
lock(&self.routing).touch(&peer, now_ms());
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
Ok(Err(_)) => {
|
Ok(Err(_)) => Err(ArtistDhtError::Protocol("response channel closed".into())),
|
||||||
self.pending.remove(&request_id);
|
Err(_) => Err(ArtistDhtError::Timeout),
|
||||||
Err(ArtistDhtError::Protocol("response channel closed".into()))
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
self.pending.remove(&request_id);
|
|
||||||
Err(ArtistDhtError::Timeout)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,13 +602,15 @@ impl Node {
|
|||||||
///
|
///
|
||||||
/// With `find_value: None` this is a node lookup converging on the
|
/// With `find_value: None` this is a node lookup converging on the
|
||||||
/// closest known nodes to `target`; with `Some(key)` it sends `FindValue`
|
/// closest known nodes to `target`; with `Some(key)` it sends `FindValue`
|
||||||
/// and stops as soon as records are found. Never broadcasts: at most
|
/// and returns as soon as the **first** records arrive — in-flight
|
||||||
/// [`ALPHA`] requests run concurrently and at most
|
/// requests to slower or unreachable peers are cancelled instead of
|
||||||
/// [`MAX_LOOKUP_REQUESTS`] are sent in total, all bounded by the lookup
|
/// awaited. Contacts in dial backoff are skipped. Never broadcasts: at
|
||||||
/// timeout.
|
/// most [`ALPHA`] requests run concurrently and at most
|
||||||
|
/// [`MAX_LOOKUP_REQUESTS`] are sent in total, all hard-bounded by the
|
||||||
|
/// lookup timeout.
|
||||||
pub async fn lookup(&self, target: [u8; 32], find_value: Option<DhtKey>) -> LookupOutcome {
|
pub async fn lookup(&self, target: [u8; 32], find_value: Option<DhtKey>) -> LookupOutcome {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let deadline = started + self.config.lookup_timeout;
|
let deadline = tokio::time::Instant::now() + self.config.lookup_timeout;
|
||||||
let mut candidates: Vec<NodeContact> = lock(&self.routing).closest(&target, K);
|
let mut candidates: Vec<NodeContact> = lock(&self.routing).closest(&target, K);
|
||||||
let mut known: HashSet<EndpointId> =
|
let mut known: HashSet<EndpointId> =
|
||||||
candidates.iter().map(|contact| contact.peer_id).collect();
|
candidates.iter().map(|contact| contact.peer_id).collect();
|
||||||
@@ -531,16 +621,20 @@ impl Node {
|
|||||||
|
|
||||||
debug!(target = %NodeId::from_bytes(target), seeds = candidates.len(), "lookup started");
|
debug!(target = %NodeId::from_bytes(target), seeds = candidates.len(), "lookup started");
|
||||||
|
|
||||||
loop {
|
'rounds: loop {
|
||||||
if Instant::now() >= deadline {
|
if tokio::time::Instant::now() >= deadline {
|
||||||
debug!("lookup deadline reached");
|
debug!("lookup deadline reached");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
candidates.sort_by_key(|contact| distance(contact.node_id.as_bytes(), &target));
|
candidates.sort_by_key(|contact| distance(contact.node_id.as_bytes(), &target));
|
||||||
let budget = MAX_LOOKUP_REQUESTS.saturating_sub(sent);
|
let budget = MAX_LOOKUP_REQUESTS.saturating_sub(sent);
|
||||||
|
let round_now = now_ms();
|
||||||
let batch: Vec<NodeContact> = candidates
|
let batch: Vec<NodeContact> = candidates
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|contact| !queried.contains(&contact.peer_id))
|
.filter(|contact| {
|
||||||
|
!queried.contains(&contact.peer_id)
|
||||||
|
&& !self.dial_backoff_active(&contact.peer_id, round_now)
|
||||||
|
})
|
||||||
.take(ALPHA.min(budget))
|
.take(ALPHA.min(budget))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
@@ -552,19 +646,25 @@ impl Node {
|
|||||||
queried.insert(contact.peer_id);
|
queried.insert(contact.peer_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let futures = batch.iter().map(|contact| {
|
// Process responses as they complete: one dead contact must not
|
||||||
let request = match find_value {
|
// hold back the answers of the live ones.
|
||||||
Some(key) => OutboundRequest::FindValue(FindValueRequest { key }),
|
let mut in_flight: FuturesUnordered<_> = batch
|
||||||
None => OutboundRequest::FindNode(FindNodeRequest {
|
.iter()
|
||||||
target: NodeId::from_bytes(target),
|
.map(|contact| async move {
|
||||||
}),
|
let request = match find_value {
|
||||||
|
Some(key) => OutboundRequest::FindValue(FindValueRequest { key }),
|
||||||
|
None => OutboundRequest::FindNode(FindNodeRequest {
|
||||||
|
target: NodeId::from_bytes(target),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
(contact, self.request(contact, request).await)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
while let Ok(next) = tokio::time::timeout_at(deadline, in_flight.next()).await {
|
||||||
|
let Some((contact, result)) = next else {
|
||||||
|
break; // The round is complete.
|
||||||
};
|
};
|
||||||
self.request(contact, request)
|
let mut found_records = false;
|
||||||
});
|
|
||||||
let results = join_all(futures).await;
|
|
||||||
|
|
||||||
let mut found_records = false;
|
|
||||||
for (contact, result) in batch.iter().zip(results) {
|
|
||||||
let nodes = match result {
|
let nodes = match result {
|
||||||
Ok(DhtResponse::FindNode(response)) => response.nodes,
|
Ok(DhtResponse::FindNode(response)) => response.nodes,
|
||||||
Ok(DhtResponse::FindValue(FindValueResponse::Records { records: found })) => {
|
Ok(DhtResponse::FindValue(FindValueResponse::Records { records: found })) => {
|
||||||
@@ -599,8 +699,13 @@ impl Node {
|
|||||||
last_seen_ms: now_ms(),
|
last_seen_ms: now_ms(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if find_value.is_some() && found_records {
|
||||||
|
// Dropping `in_flight` cancels the outstanding requests.
|
||||||
|
break 'rounds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if find_value.is_some() && found_records {
|
if tokio::time::Instant::now() >= deadline {
|
||||||
|
debug!("lookup deadline reached");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if sent >= MAX_LOOKUP_REQUESTS {
|
if sent >= MAX_LOOKUP_REQUESTS {
|
||||||
@@ -609,6 +714,10 @@ impl Node {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The closest set feeds publishes (store targets); contacts that are
|
||||||
|
// currently backed off would only burn a dial timeout each.
|
||||||
|
let closest_now = now_ms();
|
||||||
|
candidates.retain(|contact| !self.dial_backoff_active(&contact.peer_id, closest_now));
|
||||||
candidates.sort_by_key(|contact| distance(contact.node_id.as_bytes(), &target));
|
candidates.sort_by_key(|contact| distance(contact.node_id.as_bytes(), &target));
|
||||||
candidates.truncate(K);
|
candidates.truncate(K);
|
||||||
info!(
|
info!(
|
||||||
@@ -779,6 +888,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dial_backoff_doubles_and_is_capped() {
|
||||||
|
assert_eq!(dial_backoff(1), Duration::from_secs(30));
|
||||||
|
assert_eq!(dial_backoff(2), Duration::from_secs(60));
|
||||||
|
assert_eq!(dial_backoff(3), Duration::from_secs(120));
|
||||||
|
assert_eq!(dial_backoff(5), Duration::from_secs(480));
|
||||||
|
// Capped at the maximum from the 6th failure on, even for huge counts.
|
||||||
|
assert_eq!(dial_backoff(6), DIAL_BACKOFF_MAX);
|
||||||
|
assert_eq!(dial_backoff(u32::MAX), DIAL_BACKOFF_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn peer_exchange_drops_duplicates_self_and_sender() {
|
fn peer_exchange_drops_duplicates_self_and_sender() {
|
||||||
let own = test_peer(1);
|
let own = test_peer(1);
|
||||||
|
|||||||
@@ -88,12 +88,34 @@ impl PendingRequests {
|
|||||||
lock(&self.map).remove(request_id);
|
lock(&self.map).remove(request_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a guard that removes the entry when dropped, making a request
|
||||||
|
/// future safe to cancel (e.g. when a lookup exits early). Removing an
|
||||||
|
/// already-completed entry is a no-op.
|
||||||
|
pub fn remove_on_drop(&self, request_id: RequestId) -> PendingCleanup<'_> {
|
||||||
|
PendingCleanup {
|
||||||
|
pending: self,
|
||||||
|
request_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Number of currently pending requests.
|
/// Number of currently pending requests.
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
lock(&self.map).len()
|
lock(&self.map).len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes a pending entry on drop; see [`PendingRequests::remove_on_drop`].
|
||||||
|
pub(crate) struct PendingCleanup<'a> {
|
||||||
|
pending: &'a PendingRequests,
|
||||||
|
request_id: RequestId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PendingCleanup<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.pending.remove(&self.request_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||||
mutex
|
mutex
|
||||||
.lock()
|
.lock()
|
||||||
|
|||||||
@@ -165,6 +165,17 @@ impl RoutingTable {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes a contact by its transport id. Returns `true` if it was known.
|
||||||
|
pub fn remove(&mut self, peer_id: &EndpointId) -> bool {
|
||||||
|
for bucket in &mut self.buckets {
|
||||||
|
if let Some(index) = bucket.iter().position(|entry| &entry.peer_id == peer_id) {
|
||||||
|
bucket.remove(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Looks up a contact by its transport id.
|
/// Looks up a contact by its transport id.
|
||||||
pub fn get(&self, peer_id: &EndpointId) -> Option<NodeContact> {
|
pub fn get(&self, peer_id: &EndpointId) -> Option<NodeContact> {
|
||||||
self.buckets
|
self.buckets
|
||||||
@@ -303,6 +314,18 @@ mod tests {
|
|||||||
assert!(contacts.iter().all(|c| c.last_seen_ms >= 4));
|
assert!(contacts.iter().all(|c| c.last_seen_ms >= 4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_deletes_contact() {
|
||||||
|
let own = NodeId::from_bytes([0u8; 32]);
|
||||||
|
let mut table = RoutingTable::new(own);
|
||||||
|
table.upsert(contact(1, 10));
|
||||||
|
table.upsert(contact(2, 10));
|
||||||
|
assert!(table.remove(&test_peer(1)));
|
||||||
|
assert!(!table.remove(&test_peer(1)));
|
||||||
|
assert_eq!(table.len(), 1);
|
||||||
|
assert!(table.get(&test_peer(1)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn upsert_refreshes_existing_contact() {
|
fn upsert_refreshes_existing_contact() {
|
||||||
let own = NodeId::from_bytes([0u8; 32]);
|
let own = NodeId::from_bytes([0u8; 32]);
|
||||||
|
|||||||
Reference in New Issue
Block a user