diff --git a/Cargo.lock b/Cargo.lock index 2fbf628713b..f510267ca06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3372,12 +3372,14 @@ name = "libp2p-yamux" version = "0.45.1" dependencies = [ "async-std", + "either", "futures", "libp2p-core", "libp2p-muxer-test-harness", "thiserror", "tracing", - "yamux", + "yamux 0.12.1", + "yamux 0.13.1", ] [[package]] @@ -6862,6 +6864,22 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "yamux" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1d0148b89300047e72994bee99ecdabd15a9166a7b70c8b8c37c314dcc9002" +dependencies = [ + "futures", + "instant", + "log", + "nohash-hasher", + "parking_lot", + "pin-project", + "rand 0.8.5", + "static_assertions", +] + [[package]] name = "yasna" version = "0.5.2" diff --git a/muxers/yamux/CHANGELOG.md b/muxers/yamux/CHANGELOG.md index c8534166ea6..de608b195f8 100644 --- a/muxers/yamux/CHANGELOG.md +++ b/muxers/yamux/CHANGELOG.md @@ -4,6 +4,12 @@ It does not enforce flow-control, i.e. breaks backpressure. Use `WindowUpdateMode::on_read` instead. See `yamux` crate version `v0.12.1` and [Yamux PR #177](https://github.com/libp2p/rust-yamux/pull/177). +- `yamux` `v0.13` enables auto-tuning for the Yamux stream receive window. + While preserving small buffers on low-latency and/or low-bandwidth connections, this change allows for high-latency and/or high-bandwidth connections to exhaust the available bandwidth on a single stream. + Have `libp2p-yamux` use `yamux` `v0.13` (new version) by default and fall back to `yamux` `v0.12` (old version) when setting any configuration options. + Thus default users benefit from the increased performance, while power users with custom configurations maintain the old behavior. + `libp2p-yamux` will switch over to `yamux` `v0.13` entirely with the next breaking release. + See [PR 4970](https://github.com/libp2p/rust-libp2p/pull/4970). ## 0.45.0 diff --git a/muxers/yamux/Cargo.toml b/muxers/yamux/Cargo.toml index 1456238121b..36601ae56af 100644 --- a/muxers/yamux/Cargo.toml +++ b/muxers/yamux/Cargo.toml @@ -11,10 +11,12 @@ keywords = ["peer-to-peer", "libp2p", "networking"] categories = ["network-programming", "asynchronous"] [dependencies] +either = "1" futures = "0.3.29" libp2p-core = { workspace = true } thiserror = "1.0" -yamux = "0.12" +yamux012 = { version = "0.12.1", package = "yamux" } +yamux013 = { version = "0.13.1", package = "yamux" } tracing = "0.1.37" [dev-dependencies] diff --git a/muxers/yamux/src/lib.rs b/muxers/yamux/src/lib.rs index fc7ff430396..2b5eb52a11e 100644 --- a/muxers/yamux/src/lib.rs +++ b/muxers/yamux/src/lib.rs @@ -22,6 +22,7 @@ #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +use either::Either; use futures::{future, prelude::*, ready}; use libp2p_core::muxing::{StreamMuxer, StreamMuxerEvent}; use libp2p_core::upgrade::{InboundConnectionUpgrade, OutboundConnectionUpgrade, UpgradeInfo}; @@ -34,15 +35,14 @@ use std::{ task::{Context, Poll}, }; use thiserror::Error; -use yamux::ConnectionError; /// A Yamux connection. #[derive(Debug)] pub struct Muxer { - connection: yamux::Connection, + connection: Either, yamux013::Connection>, /// Temporarily buffers inbound streams in case our node is performing backpressure on the remote. /// - /// The only way how yamux can make progress is by calling [`yamux::Connection::poll_next_inbound`]. However, the + /// The only way how yamux can make progress is by calling [`yamux013::Connection::poll_next_inbound`]. However, the /// [`StreamMuxer`] interface is designed to allow a caller to selectively make progress via /// [`StreamMuxer::poll_inbound`] and [`StreamMuxer::poll_outbound`] whilst the more general /// [`StreamMuxer::poll`] is designed to make progress on existing streams etc. @@ -65,9 +65,9 @@ where C: AsyncRead + AsyncWrite + Send + Unpin + 'static, { /// Create a new Yamux connection. - fn new(io: C, cfg: yamux::Config, mode: yamux::Mode) -> Self { + fn new(connection: Either, yamux013::Connection>) -> Self { Muxer { - connection: yamux::Connection::new(io, cfg, mode), + connection, inbound_stream_buffer: VecDeque::default(), inbound_stream_waker: None, } @@ -103,16 +103,23 @@ where mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - let stream = ready!(self.connection.poll_new_outbound(cx).map_err(Error)?); - - Poll::Ready(Ok(Stream(stream))) + let stream = match self.connection.as_mut() { + Either::Left(c) => ready!(c.poll_new_outbound(cx)) + .map_err(|e| Error(Either::Left(e))) + .map(|s| Stream(Either::Left(s))), + Either::Right(c) => ready!(c.poll_new_outbound(cx)) + .map_err(|e| Error(Either::Right(e))) + .map(|s| Stream(Either::Right(s))), + }?; + Poll::Ready(Ok(stream)) } #[tracing::instrument(level = "trace", name = "StreamMuxer::poll_close", skip(self, cx))] fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - ready!(self.connection.poll_close(cx).map_err(Error)?); - - Poll::Ready(Ok(())) + match self.connection.as_mut() { + Either::Left(c) => c.poll_close(cx).map_err(|e| Error(Either::Left(e))), + Either::Right(c) => c.poll_close(cx).map_err(|e| Error(Either::Right(e))), + } } #[tracing::instrument(level = "trace", name = "StreamMuxer::poll", skip(self, cx))] @@ -146,7 +153,7 @@ where /// A stream produced by the yamux multiplexer. #[derive(Debug)] -pub struct Stream(yamux::Stream); +pub struct Stream(Either); impl AsyncRead for Stream { fn poll_read( @@ -154,7 +161,7 @@ impl AsyncRead for Stream { cx: &mut Context<'_>, buf: &mut [u8], ) -> Poll> { - Pin::new(&mut self.0).poll_read(cx, buf) + either::for_both!(self.0.as_mut(), s => Pin::new(s).poll_read(cx, buf)) } fn poll_read_vectored( @@ -162,7 +169,7 @@ impl AsyncRead for Stream { cx: &mut Context<'_>, bufs: &mut [IoSliceMut<'_>], ) -> Poll> { - Pin::new(&mut self.0).poll_read_vectored(cx, bufs) + either::for_both!(self.0.as_mut(), s => Pin::new(s).poll_read_vectored(cx, bufs)) } } @@ -172,7 +179,7 @@ impl AsyncWrite for Stream { cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - Pin::new(&mut self.0).poll_write(cx, buf) + either::for_both!(self.0.as_mut(), s => Pin::new(s).poll_write(cx, buf)) } fn poll_write_vectored( @@ -180,15 +187,15 @@ impl AsyncWrite for Stream { cx: &mut Context<'_>, bufs: &[IoSlice<'_>], ) -> Poll> { - Pin::new(&mut self.0).poll_write_vectored(cx, bufs) + either::for_both!(self.0.as_mut(), s => Pin::new(s).poll_write_vectored(cx, bufs)) } fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_flush(cx) + either::for_both!(self.0.as_mut(), s => Pin::new(s).poll_flush(cx)) } fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_close(cx) + either::for_both!(self.0.as_mut(), s => Pin::new(s).poll_close(cx)) } } @@ -197,11 +204,16 @@ where C: AsyncRead + AsyncWrite + Unpin + 'static, { fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll> { - let stream = ready!(self.connection.poll_next_inbound(cx)) - .transpose() - .map_err(Error)? - .map(Stream) - .ok_or(Error(ConnectionError::Closed))?; + let stream = match self.connection.as_mut() { + Either::Left(c) => ready!(c.poll_next_inbound(cx)) + .ok_or(Error(Either::Left(yamux012::ConnectionError::Closed)))? + .map_err(|e| Error(Either::Left(e))) + .map(|s| Stream(Either::Left(s)))?, + Either::Right(c) => ready!(c.poll_next_inbound(cx)) + .ok_or(Error(Either::Right(yamux013::ConnectionError::Closed)))? + .map_err(|e| Error(Either::Right(e))) + .map(|s| Stream(Either::Right(s)))?, + }; Poll::Ready(Ok(stream)) } @@ -209,14 +221,33 @@ where /// The yamux configuration. #[derive(Debug, Clone)] -pub struct Config { - inner: yamux::Config, - mode: Option, +pub struct Config(Either); + +impl Default for Config { + fn default() -> Self { + Self(Either::Right(Config013::default())) + } +} + +#[derive(Debug, Clone)] +struct Config012 { + inner: yamux012::Config, + mode: Option, +} + +impl Default for Config012 { + fn default() -> Self { + let mut inner = yamux012::Config::default(); + // For conformity with mplex, read-after-close on a multiplexed + // connection is never permitted and not configurable. + inner.set_read_after_close(false); + Self { inner, mode: None } + } } /// The window update mode determines when window updates are /// sent to the remote, giving it new credit to send more data. -pub struct WindowUpdateMode(yamux::WindowUpdateMode); +pub struct WindowUpdateMode(yamux012::WindowUpdateMode); impl WindowUpdateMode { /// The window update mode whereby the remote is given @@ -234,7 +265,7 @@ impl WindowUpdateMode { #[deprecated(note = "Use `WindowUpdateMode::on_read` instead.")] pub fn on_receive() -> Self { #[allow(deprecated)] - WindowUpdateMode(yamux::WindowUpdateMode::OnReceive) + WindowUpdateMode(yamux012::WindowUpdateMode::OnReceive) } /// The window update mode whereby the remote is given new @@ -252,62 +283,71 @@ impl WindowUpdateMode { /// > **Note**: With this strategy, there is usually no point in the /// > receive buffer being larger than the window size. pub fn on_read() -> Self { - WindowUpdateMode(yamux::WindowUpdateMode::OnRead) + WindowUpdateMode(yamux012::WindowUpdateMode::OnRead) } } impl Config { /// Creates a new `YamuxConfig` in client mode, regardless of whether /// it will be used for an inbound or outbound upgrade. + #[deprecated(note = "Will be removed with the next breaking release.")] pub fn client() -> Self { - Self { - mode: Some(yamux::Mode::Client), + Self(Either::Left(Config012 { + mode: Some(yamux012::Mode::Client), ..Default::default() - } + })) } /// Creates a new `YamuxConfig` in server mode, regardless of whether /// it will be used for an inbound or outbound upgrade. + #[deprecated(note = "Will be removed with the next breaking release.")] pub fn server() -> Self { - Self { - mode: Some(yamux::Mode::Server), + Self(Either::Left(Config012 { + mode: Some(yamux012::Mode::Server), ..Default::default() - } + })) } /// Sets the size (in bytes) of the receive window per substream. + #[deprecated( + note = "Will be replaced in the next breaking release with a connection receive window size limit." + )] pub fn set_receive_window_size(&mut self, num_bytes: u32) -> &mut Self { - self.inner.set_receive_window(num_bytes); - self + self.set(|cfg| cfg.set_receive_window(num_bytes)) } /// Sets the maximum size (in bytes) of the receive buffer per substream. + #[deprecated(note = "Will be removed with the next breaking release.")] pub fn set_max_buffer_size(&mut self, num_bytes: usize) -> &mut Self { - self.inner.set_max_buffer_size(num_bytes); - self + self.set(|cfg| cfg.set_max_buffer_size(num_bytes)) } /// Sets the maximum number of concurrent substreams. pub fn set_max_num_streams(&mut self, num_streams: usize) -> &mut Self { - self.inner.set_max_num_streams(num_streams); - self + self.set(|cfg| cfg.set_max_num_streams(num_streams)) } /// Sets the window update mode that determines when the remote /// is given new credit for sending more data. + #[deprecated( + note = "`WindowUpdate::OnRead` is the default. `WindowUpdate::OnReceive` breaks backpressure, is thus not recommended, and will be removed in the next breaking release. Thus this method becomes obsolete and will be removed with the next breaking release." + )] pub fn set_window_update_mode(&mut self, mode: WindowUpdateMode) -> &mut Self { - self.inner.set_window_update_mode(mode.0); - self + self.set(|cfg| cfg.set_window_update_mode(mode.0)) } -} -impl Default for Config { - fn default() -> Self { - let mut inner = yamux::Config::default(); - // For conformity with mplex, read-after-close on a multiplexed - // connection is never permitted and not configurable. - inner.set_read_after_close(false); - Config { inner, mode: None } + fn set(&mut self, f: impl FnOnce(&mut yamux012::Config) -> &mut yamux012::Config) -> &mut Self { + let cfg012 = match self.0.as_mut() { + Either::Left(c) => &mut c.inner, + Either::Right(_) => { + self.0 = Either::Left(Config012::default()); + &mut self.0.as_mut().unwrap_left().inner + } + }; + + f(cfg012); + + self } } @@ -329,8 +369,18 @@ where type Future = future::Ready>; fn upgrade_inbound(self, io: C, _: Self::Info) -> Self::Future { - let mode = self.mode.unwrap_or(yamux::Mode::Server); - future::ready(Ok(Muxer::new(io, self.inner, mode))) + let connection = match self.0 { + Either::Left(Config012 { inner, mode }) => Either::Left(yamux012::Connection::new( + io, + inner, + mode.unwrap_or(yamux012::Mode::Server), + )), + Either::Right(Config013(cfg)) => { + Either::Right(yamux013::Connection::new(io, cfg, yamux013::Mode::Server)) + } + }; + + future::ready(Ok(Muxer::new(connection))) } } @@ -343,21 +393,69 @@ where type Future = future::Ready>; fn upgrade_outbound(self, io: C, _: Self::Info) -> Self::Future { - let mode = self.mode.unwrap_or(yamux::Mode::Client); - future::ready(Ok(Muxer::new(io, self.inner, mode))) + let connection = match self.0 { + Either::Left(Config012 { inner, mode }) => Either::Left(yamux012::Connection::new( + io, + inner, + mode.unwrap_or(yamux012::Mode::Client), + )), + Either::Right(Config013(cfg)) => { + Either::Right(yamux013::Connection::new(io, cfg, yamux013::Mode::Client)) + } + }; + + future::ready(Ok(Muxer::new(connection))) + } +} + +#[derive(Debug, Clone)] +struct Config013(yamux013::Config); + +impl Default for Config013 { + fn default() -> Self { + let mut cfg = yamux013::Config::default(); + // For conformity with mplex, read-after-close on a multiplexed + // connection is never permitted and not configurable. + cfg.set_read_after_close(false); + Self(cfg) } } /// The Yamux [`StreamMuxer`] error type. #[derive(Debug, Error)] #[error(transparent)] -pub struct Error(yamux::ConnectionError); +pub struct Error(Either); impl From for io::Error { fn from(err: Error) -> Self { match err.0 { - yamux::ConnectionError::Io(e) => e, - e => io::Error::new(io::ErrorKind::Other, e), + Either::Left(err) => match err { + yamux012::ConnectionError::Io(e) => e, + e => io::Error::new(io::ErrorKind::Other, e), + }, + Either::Right(err) => match err { + yamux013::ConnectionError::Io(e) => e, + e => io::Error::new(io::ErrorKind::Other, e), + }, } } } + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn config_set_switches_to_v012() { + // By default we use yamux v0.13. Thus we provide the benefits of yamux v0.13 to all users + // that do not depend on any of the behaviors (i.e. configuration options) of v0.12. + let mut cfg = Config::default(); + assert!(matches!( + cfg, + Config(Either::Right(Config013(yamux013::Config { .. }))) + )); + + // In case a user makes any configurations, use yamux v0.12 instead. + cfg.set_max_num_streams(42); + assert!(matches!(cfg, Config(Either::Left(Config012 { .. })))); + } +}