Skip to content

Commit 93c5059

Browse files
authored
feat(swarm): smart dialing
This PR tries to port smart dialing logic from `go-libp2p` https://github.com/libp2p/go-libp2p/blob/v0.45.0/p2p/net/swarm/dial_ranker.go libp2p/go-libp2p#2260 Pull-Request: #6229.
1 parent 7f5900e commit 93c5059

6 files changed

Lines changed: 838 additions & 71 deletions

File tree

swarm/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
- Raise MSRV to 1.88.0.
77
See [PR 6273](https://github.com/libp2p/rust-libp2p/pull/6273).
88

9+
- Add smart dialing support (`with_smart_dial`).
10+
Ranks dial addresses by transport priority (QUIC > TCP, IPv6 > IPv4)
11+
with Happy Eyeballs (RFC 8305) staggered delays. All addresses are
12+
dialed concurrently.
13+
See [PR 6229](https://github.com/libp2p/rust-libp2p/pull/6229).
14+
915
## 0.47.1
1016

1117
- Replace `lru::LruCache` with `hashlink::LruCache`.

swarm/src/connection/pool.rs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
1919
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2020
// DEALINGS IN THE SOFTWARE.
21+
2122
use std::{
2223
collections::HashMap,
2324
convert::Infallible,
@@ -31,7 +32,7 @@ use concurrent_dial::ConcurrentDial;
3132
use fnv::FnvHashMap;
3233
use futures::{
3334
channel::{mpsc, oneshot},
34-
future::{BoxFuture, Either, poll_fn},
35+
future::{Either, poll_fn},
3536
prelude::*,
3637
ready,
3738
stream::{FuturesUnordered, SelectAll},
@@ -49,11 +50,13 @@ use crate::{
4950
connection::{
5051
Connected, Connection, ConnectionError, ConnectionId, IncomingInfo,
5152
PendingInboundConnectionError, PendingOutboundConnectionError, PendingPoint,
53+
pool::concurrent_dial::{PendingDial, SmartDial},
5254
},
5355
transport::TransportError,
5456
};
5557

56-
mod concurrent_dial;
58+
pub(crate) mod concurrent_dial;
59+
pub(crate) mod dial_ranker;
5760
mod task;
5861

5962
enum ExecSwitch {
@@ -142,6 +145,9 @@ where
142145

143146
/// How long a connection should be kept alive once it starts idling.
144147
idle_connection_timeout: Duration,
148+
149+
/// Enables smart dialing.
150+
smart_dial: bool,
145151
}
146152

147153
#[derive(Debug)]
@@ -333,6 +339,7 @@ where
333339
no_established_connections_waker: None,
334340
established_connection_events: Default::default(),
335341
new_connection_dropped_listeners: Default::default(),
342+
smart_dial: config.smart_dial,
336343
}
337344
}
338345

@@ -413,15 +420,7 @@ where
413420
/// that establishes and negotiates the connection.
414421
pub(crate) fn add_outgoing(
415422
&mut self,
416-
dials: Vec<
417-
BoxFuture<
418-
'static,
419-
(
420-
Multiaddr,
421-
Result<(PeerId, StreamMuxerBox), TransportError<std::io::Error>>,
422-
),
423-
>,
424-
>,
423+
dials: Vec<PendingDial>,
425424
peer: Option<PeerId>,
426425
role_override: Endpoint,
427426
port_use: PortUse,
@@ -435,15 +434,27 @@ where
435434

436435
let (abort_notifier, abort_receiver) = oneshot::channel();
437436

438-
self.executor.spawn(
439-
task::new_for_pending_outgoing_connection(
440-
connection_id,
441-
ConcurrentDial::new(dials, concurrency_factor),
442-
abort_receiver,
443-
self.pending_connection_events_tx.clone(),
444-
)
445-
.instrument(span),
446-
);
437+
if self.smart_dial {
438+
self.executor.spawn(
439+
task::new_for_pending_outgoing_connection(
440+
connection_id,
441+
SmartDial::new(dials),
442+
abort_receiver,
443+
self.pending_connection_events_tx.clone(),
444+
)
445+
.instrument(span),
446+
);
447+
} else {
448+
self.executor.spawn(
449+
task::new_for_pending_outgoing_connection(
450+
connection_id,
451+
ConcurrentDial::new(dials, concurrency_factor),
452+
abort_receiver,
453+
self.pending_connection_events_tx.clone(),
454+
)
455+
.instrument(span),
456+
);
457+
}
447458

448459
let endpoint = PendingPoint::Dialer {
449460
role_override,
@@ -984,6 +995,8 @@ pub(crate) struct PoolConfig {
984995
pub(crate) per_connection_event_buffer_size: usize,
985996
/// Number of addresses concurrently dialed for a single outbound connection attempt.
986997
pub(crate) dial_concurrency_factor: NonZeroU8,
998+
/// Ranker that determines the ranking of outgoing connection attempts.
999+
pub(crate) smart_dial: bool,
9871000
/// How long a connection should be kept alive once it is idling.
9881001
pub(crate) idle_connection_timeout: Duration,
9891002
/// The configured override for substream protocol upgrades, if any.
@@ -1005,6 +1018,7 @@ impl PoolConfig {
10051018
idle_connection_timeout: Duration::from_secs(10),
10061019
substream_upgrade_protocol_override: None,
10071020
max_negotiating_inbound_streams: 128,
1021+
smart_dial: false,
10081022
}
10091023
}
10101024

swarm/src/connection/pool/concurrent_dial.rs

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,66 +22,96 @@ use std::{
2222
num::NonZeroU8,
2323
pin::Pin,
2424
task::{Context, Poll},
25+
vec::IntoIter,
2526
};
2627

2728
use futures::{
29+
FutureExt,
2830
future::{BoxFuture, Future},
2931
ready,
3032
stream::{FuturesUnordered, StreamExt},
3133
};
34+
use futures_timer::Delay;
3235
use libp2p_core::muxing::StreamMuxerBox;
3336
use libp2p_identity::PeerId;
3437

35-
use crate::{Multiaddr, transport::TransportError};
38+
use crate::{Multiaddr, connection::pool::dial_ranker::rank_dials, transport::TransportError};
3639

37-
type Dial = BoxFuture<
40+
/// A pending outbound dial that hasn't started yet.
41+
///
42+
/// The `fut` is created upfront by `Transport::dial()` but won't be polled
43+
/// until `delay` elapses. If `smart_dial` is enabled, `rank_dials` sets the
44+
/// `delay` and reorders the vec so preferred addresses start first. Without
45+
/// smart dial, delays are `None` and the concurrency factor limits how many
46+
/// are polled at once.
47+
pub(crate) struct PendingDial {
48+
pub(crate) addr: Multiaddr,
49+
pub(crate) fut: DialFuture,
50+
}
51+
52+
/// The async dial operation. Owns the actual transport dial and returns the
53+
/// dialed address along with the result so the pool can match failures
54+
/// back to specific addresses.
55+
pub(crate) type DialFuture = BoxFuture<
3856
'static,
3957
(
4058
Multiaddr,
4159
Result<(PeerId, StreamMuxerBox), TransportError<std::io::Error>>,
4260
),
4361
>;
4462

63+
/// The result of a concurrent or smart dial to a single peer.
64+
///
65+
/// Returns the first successful address and its negotiated connection, along
66+
/// with any errors from earlier failed dials. Returns all errors if every
67+
/// dial failed.
68+
pub(crate) type DialResult = Result<
69+
(
70+
Multiaddr,
71+
(PeerId, StreamMuxerBox),
72+
Vec<(Multiaddr, TransportError<std::io::Error>)>,
73+
),
74+
Vec<(Multiaddr, TransportError<std::io::Error>)>,
75+
>;
76+
77+
/// Drives concurrent dial attempts to a single peer, limited by a concurrency factor.
78+
///
79+
/// Starts up to `concurrency_factor` dials simultaneously. On failure, the next
80+
/// pending dial starts immediately, keeping the concurrency window filled until
81+
/// either a dial succeeds or all pending dials are exhausted.
4582
pub(crate) struct ConcurrentDial {
46-
dials: FuturesUnordered<Dial>,
47-
pending_dials: Box<dyn Iterator<Item = Dial> + Send>,
83+
dials: FuturesUnordered<DialFuture>,
84+
pending_dials: IntoIter<PendingDial>,
4885
errors: Vec<(Multiaddr, TransportError<std::io::Error>)>,
4986
}
5087

5188
impl Unpin for ConcurrentDial {}
5289

5390
impl ConcurrentDial {
54-
pub(crate) fn new(pending_dials: Vec<Dial>, concurrency_factor: NonZeroU8) -> Self {
91+
pub(crate) fn new(pending_dials: Vec<PendingDial>, concurrency_factor: NonZeroU8) -> Self {
5592
let mut pending_dials = pending_dials.into_iter();
5693

94+
// Fill the concurrency window: start up to `concurrency_factor` dials.
95+
// As each dial completes (success or failure), `dial_pending()` is called
96+
// to refill the window from remaining pending dials.
5797
let dials = FuturesUnordered::new();
58-
for dial in pending_dials.by_ref() {
59-
dials.push(dial);
60-
if dials.len() == concurrency_factor.get() as usize {
61-
break;
62-
}
98+
for dial in pending_dials
99+
.by_ref()
100+
.take(concurrency_factor.get() as usize)
101+
{
102+
dials.push(dial.fut);
63103
}
64104

65105
Self {
66106
dials,
67107
errors: Default::default(),
68-
pending_dials: Box::new(pending_dials),
108+
pending_dials,
69109
}
70110
}
71111
}
72112

73113
impl Future for ConcurrentDial {
74-
type Output = Result<
75-
// Either one dial succeeded, returning the negotiated [`PeerId`], the address, the
76-
// muxer and the addresses and errors of the dials that failed before.
77-
(
78-
Multiaddr,
79-
(PeerId, StreamMuxerBox),
80-
Vec<(Multiaddr, TransportError<std::io::Error>)>,
81-
),
82-
// Or all dials failed, thus returning the address and error for each dial.
83-
Vec<(Multiaddr, TransportError<std::io::Error>)>,
84-
>;
114+
type Output = DialResult;
85115

86116
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
87117
loop {
@@ -93,8 +123,66 @@ impl Future for ConcurrentDial {
93123
Some((addr, Err(e))) => {
94124
self.errors.push((addr, e));
95125
if let Some(dial) = self.pending_dials.next() {
96-
self.dials.push(dial)
126+
self.dials.push(dial.fut);
127+
}
128+
}
129+
None => {
130+
return Poll::Ready(Err(std::mem::take(&mut self.errors)));
131+
}
132+
}
133+
}
134+
}
135+
}
136+
137+
/// Drives ranked dial attempts to a single peer with staggered delays.
138+
///
139+
/// All dials start immediately via [`rank_dials`], which assigns delays based on
140+
/// transport priority (QUIC > TCP, IPv6 > IPv4) and Happy Eyeballs (RFC 8305).
141+
/// The delays pace the dials, giving faster transports a head start while slower
142+
/// paths wait, with all dials overlapping in flight.
143+
pub(crate) struct SmartDial {
144+
dials: FuturesUnordered<DialFuture>,
145+
errors: Vec<(Multiaddr, TransportError<std::io::Error>)>,
146+
}
147+
148+
impl Unpin for SmartDial {}
149+
150+
impl SmartDial {
151+
pub(crate) fn new(pending_dials: Vec<PendingDial>) -> Self {
152+
let pending_dials = rank_dials(pending_dials);
153+
154+
let dials = FuturesUnordered::new();
155+
for (delay, dial) in pending_dials {
156+
dials.push(
157+
async move {
158+
if !delay.is_zero() {
159+
Delay::new(delay).await;
97160
}
161+
dial.fut.await
162+
}
163+
.boxed(),
164+
);
165+
}
166+
167+
Self {
168+
dials,
169+
errors: Default::default(),
170+
}
171+
}
172+
}
173+
174+
impl Future for SmartDial {
175+
type Output = DialResult;
176+
177+
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
178+
loop {
179+
match ready!(self.dials.poll_next_unpin(cx)) {
180+
Some((addr, Ok(output))) => {
181+
let errors = std::mem::take(&mut self.errors);
182+
return Poll::Ready(Ok((addr, output, errors)));
183+
}
184+
Some((addr, Err(e))) => {
185+
self.errors.push((addr, e));
98186
}
99187
None => {
100188
return Poll::Ready(Err(std::mem::take(&mut self.errors)));

0 commit comments

Comments
 (0)