Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ libp2p-rendezvous = { version = "0.18.0", path = "protocols/rendezvous" }
libp2p-request-response = { version = "0.30.0", path = "protocols/request-response" }
libp2p-server = { version = "0.13.0", path = "misc/server" }
libp2p-stream = { version = "0.5.0-alpha", path = "protocols/stream" }
libp2p-swarm = { version = "0.48.0", path = "swarm" }
libp2p-swarm = { version = "0.48.1", path = "swarm" }
Comment thread
akshitj11 marked this conversation as resolved.
Outdated
libp2p-swarm-derive = { version = "=0.36.0", path = "swarm-derive" } # `libp2p-swarm-derive` may not be compatible with different `libp2p-swarm` non-breaking releases. E.g. `libp2p-swarm` might introduce a new enum variant `FromSwarm` (which is `#[non-exhaustive]`) in a non-breaking release. Older versions of `libp2p-swarm-derive` would not forward this enum variant within the `NetworkBehaviour` hierarchy. Thus the version pinning is required.
libp2p-swarm-test = { version = "0.7.0", path = "swarm-test" }
libp2p-tcp = { version = "0.45.0", path = "transports/tcp" }
Expand Down
5 changes: 5 additions & 0 deletions swarm/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.48.1

- Bound `Connection::poll` fixed-point loop iterations to improve cooperative scheduling under load.
See [Issue 6438](https://github.com/libp2p/rust-libp2p/issues/6438).

## 0.48.0

- Remove `wasm-bindgen` feature and make `wasm` support implicit.
Expand Down
2 changes: 1 addition & 1 deletion swarm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "libp2p-swarm"
edition.workspace = true
rust-version = { workspace = true }
description = "The libp2p swarm"
version = "0.48.0"
version = "0.48.1"
authors = ["Parity Technologies <admin@parity.io>"]
license = "MIT"
repository = "https://github.com/libp2p/rust-libp2p"
Expand Down
115 changes: 97 additions & 18 deletions swarm/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ use crate::{
upgrade::{InboundUpgradeSend, OutboundUpgradeSend},
};

/// Maximum number of internal progress iterations per [`Connection::poll`] call.
///
/// When exhausted, the connection yields with a waker notification so the executor
/// can schedule other tasks. Aligned with Tokio's default cooperative budget (128).
const CONNECTION_POLL_ITERATION_BUDGET: u32 = 128;
Comment thread
akshitj11 marked this conversation as resolved.

macro_rules! continue_after_poll_progress {
($budget:expr, $cx:expr) => {{
$budget -= 1;
if $budget == 0 {
$cx.waker().wake_by_ref();
return Poll::Pending;
}
continue;
}};
}
Comment thread
akshitj11 marked this conversation as resolved.
Outdated

static NEXT_CONNECTION_ID: AtomicUsize = AtomicUsize::new(1);

/// Connection identifier.
Expand Down Expand Up @@ -271,17 +288,19 @@ where
..
} = self.get_mut();

let mut poll_budget = CONNECTION_POLL_ITERATION_BUDGET;

loop {
match requested_substreams.poll_next_unpin(cx) {
Poll::Ready(Some(Ok(()))) => continue,
Poll::Ready(Some(Ok(()))) => continue_after_poll_progress!(poll_budget, cx),
Poll::Ready(Some(Err(info))) => {
handler.on_connection_event(ConnectionEvent::DialUpgradeError(
DialUpgradeError {
info,
error: StreamUpgradeError::Timeout,
},
));
continue;
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(None) | Poll::Pending => {}
}
Expand All @@ -294,7 +313,7 @@ where
let (upgrade, user_data) = protocol.into_upgrade();

requested_substreams.push(SubstreamRequested::new(user_data, timeout, upgrade));
continue; // Poll handler until exhausted.
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(event)) => {
return Poll::Ready(Ok(Event::Handler(event)));
Expand All @@ -308,7 +327,7 @@ where
handler.on_connection_event(ConnectionEvent::RemoteProtocolsChange(added));
remote_supported_protocols.extend(protocol_buffer.drain(..));
}
continue;
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(ConnectionHandlerEvent::ReportRemoteProtocols(
ProtocolSupport::Removed(protocols),
Expand All @@ -321,7 +340,7 @@ where
handler
.on_connection_event(ConnectionEvent::RemoteProtocolsChange(removed));
}
continue;
continue_after_poll_progress!(poll_budget, cx);
}
}

Expand All @@ -333,13 +352,13 @@ where
handler.on_connection_event(ConnectionEvent::FullyNegotiatedOutbound(
FullyNegotiatedOutbound { protocol, info },
));
continue;
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(Some((info, Err(error)))) => {
handler.on_connection_event(ConnectionEvent::DialUpgradeError(
DialUpgradeError { info, error },
));
continue;
continue_after_poll_progress!(poll_budget, cx);
}
}

Expand All @@ -351,25 +370,25 @@ where
handler.on_connection_event(ConnectionEvent::FullyNegotiatedInbound(
FullyNegotiatedInbound { protocol, info },
));
continue;
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(Some((info, Err(StreamUpgradeError::Apply(error))))) => {
handler.on_connection_event(ConnectionEvent::ListenUpgradeError(
ListenUpgradeError { info, error },
));
continue;
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(Some((_, Err(StreamUpgradeError::Io(e))))) => {
tracing::debug!("failed to upgrade inbound stream: {e}");
continue;
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(Some((_, Err(StreamUpgradeError::NegotiationFailed)))) => {
tracing::debug!("no protocol could be agreed upon for inbound stream");
continue;
continue_after_poll_progress!(poll_budget, cx);
}
Poll::Ready(Some((_, Err(StreamUpgradeError::Timeout)))) => {
tracing::debug!("inbound stream upgrade timed out");
continue;
continue_after_poll_progress!(poll_budget, cx);
}
}

Expand Down Expand Up @@ -428,7 +447,7 @@ where

// Go back to the top,
// handler can potentially make progress again.
continue;
continue_after_poll_progress!(poll_budget, cx);
}
}
}
Expand All @@ -447,7 +466,7 @@ where

// Go back to the top,
// handler can potentially make progress again.
continue;
continue_after_poll_progress!(poll_budget, cx);
}
}
}
Expand All @@ -463,7 +482,7 @@ where
handler.on_connection_event(ConnectionEvent::LocalProtocolsChange(change));
}
// Go back to the top, handler can potentially make progress again.
continue;
continue_after_poll_progress!(poll_budget, cx);
}

// Nothing can make progress, return `Pending`.
Expand Down Expand Up @@ -783,7 +802,11 @@ impl<T: AsRef<str>> std::hash::Hash for AsStrHashEq<T> {
mod tests {
use std::{
convert::Infallible,
sync::{Arc, Weak},
sync::{
Arc, Weak,
atomic::{AtomicUsize, Ordering},
},
task::{Context, RawWaker, RawWakerVTable, Waker},
time::Instant,
};

Expand Down Expand Up @@ -818,9 +841,9 @@ mod tests {
Duration::ZERO,
);

let result = connection.poll_noop_waker();
while connection.poll_noop_waker().is_ready() {}

assert!(result.is_pending());
assert!(connection.poll_noop_waker().is_pending());
assert_eq!(
Arc::weak_count(&alive_substream_counter),
max_negotiating_inbound_streams,
Expand All @@ -831,6 +854,62 @@ mod tests {
QuickCheck::new().quickcheck(prop as fn(_));
}

#[test]
fn poll_budget_yields_and_wakes() {
static WAKE_COUNT: AtomicUsize = AtomicUsize::new(0);

fn counting_waker() -> Waker {
unsafe fn clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &VTABLE)
}
unsafe fn wake(_: *const ()) {
WAKE_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe fn wake_by_ref(_: *const ()) {
WAKE_COUNT.fetch_add(1, Ordering::SeqCst);
}
unsafe fn drop(_: *const ()) {}

static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, wake, wake_by_ref, drop);

unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) }
}

WAKE_COUNT.store(0, Ordering::SeqCst);

let max_negotiating_inbound_streams = (CONNECTION_POLL_ITERATION_BUDGET + 64) as usize;
let alive_substream_counter = Arc::new(());
let mut connection = Connection::new(
StreamMuxerBox::new(DummyStreamMuxer {
counter: alive_substream_counter.clone(),
}),
MockConnectionHandler::new(Duration::from_secs(10)),
None,
max_negotiating_inbound_streams,
Duration::ZERO,
);

let waker = counting_waker();
let mut cx = Context::from_waker(&waker);

assert!(Pin::new(&mut connection).poll(&mut cx).is_pending());
assert!(
WAKE_COUNT.load(Ordering::SeqCst) >= 1,
"expected waker notification when poll budget is exhausted"
);
assert!(
Arc::weak_count(&alive_substream_counter) <= CONNECTION_POLL_ITERATION_BUDGET as usize,
);

while Pin::new(&mut connection).poll(&mut cx).is_ready() {}

assert!(Pin::new(&mut connection).poll(&mut cx).is_pending());
assert_eq!(
Arc::weak_count(&alive_substream_counter),
max_negotiating_inbound_streams,
);
}

#[test]
fn outbound_stream_timeout_starts_on_request() {
let upgrade_timeout = Duration::from_secs(1);
Expand Down