Skip to content

Commit 30c5457

Browse files
committed
feat(stream): add Control::open_stream_on_connection
`open_stream` picks one of a peer's connections at random. This adds a variant that opens on a caller-specified connection, so a flow can be routed over a chosen connection/path. `Shared` already tracked per-connection senders; this exposes one via `Shared::sender_on`. Signed-off-by: Royyan Zahir <muhammad.royyan@tii.ae>
1 parent 50306da commit 30c5457

4 files changed

Lines changed: 104 additions & 2 deletions

File tree

protocols/stream/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 0.5.0-alpha
22

3+
- Add `Control::open_stream_on_connection` to open an outbound stream on a
4+
specific connection instead of a randomly chosen one.
5+
See [PR 6487](https://github.com/libp2p/rust-libp2p/pull/6487).
36
- Raise MSRV to 1.88.0.
47
See [PR 6273](https://github.com/libp2p/rust-libp2p/pull/6273).
58

protocols/stream/src/control.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use futures::{
1111
channel::{mpsc, oneshot},
1212
};
1313
use libp2p_identity::PeerId;
14-
use libp2p_swarm::{Stream, StreamProtocol};
14+
use libp2p_swarm::{ConnectionId, Stream, StreamProtocol};
1515

1616
use crate::{AlreadyRegistered, handler::NewStream, shared::Shared};
1717

@@ -64,6 +64,37 @@ impl Control {
6464
Ok(stream)
6565
}
6666

67+
/// Like [`Control::open_stream`] but opens on a specific `connection`
68+
/// instead of picking one of the peer's connections. Errors if that
69+
/// connection is not, or no longer, established.
70+
pub async fn open_stream_on_connection(
71+
&mut self,
72+
peer: PeerId,
73+
connection: ConnectionId,
74+
protocol: StreamProtocol,
75+
) -> Result<Stream, OpenStreamError> {
76+
tracing::debug!(%peer, %connection, "Requesting new stream on connection");
77+
78+
let mut new_stream_sender = Shared::lock(&self.shared)
79+
.sender_on(connection)
80+
.ok_or_else(|| {
81+
io::Error::new(io::ErrorKind::NotConnected, "connection not established")
82+
})?;
83+
84+
let (sender, receiver) = oneshot::channel();
85+
86+
new_stream_sender
87+
.send(NewStream { protocol, sender })
88+
.await
89+
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionReset, e))?;
90+
91+
let stream = receiver
92+
.await
93+
.map_err(|e| io::Error::new(io::ErrorKind::ConnectionReset, e))??;
94+
95+
Ok(stream)
96+
}
97+
6798
/// Accept inbound streams for the provided protocol.
6899
///
69100
/// To stop accepting streams, simply drop the returned [`IncomingStreams`] handle.

protocols/stream/src/shared.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ impl Shared {
122122
}
123123
}
124124

125+
/// Sender for a specific connection, if it is established.
126+
pub(crate) fn sender_on(&self, connection: ConnectionId) -> Option<mpsc::Sender<NewStream>> {
127+
self.senders.get(&connection).cloned()
128+
}
129+
125130
pub(crate) fn sender(&mut self, peer: PeerId) -> mpsc::Sender<NewStream> {
126131
let maybe_sender = self
127132
.connections

protocols/stream/tests/lib.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::io;
33
use futures::{AsyncReadExt as _, AsyncWriteExt as _, StreamExt as _};
44
use libp2p_identity::PeerId;
55
use libp2p_stream as stream;
6-
use libp2p_swarm::{StreamProtocol, Swarm};
6+
use libp2p_swarm::{ConnectionId, StreamProtocol, Swarm, SwarmEvent};
77
use libp2p_swarm_test::SwarmExt as _;
88
use stream::OpenStreamError;
99
use tracing::level_filters::LevelFilter;
@@ -78,3 +78,66 @@ async fn dial_errors_are_propagated() {
7878
assert_eq!(e.kind(), io::ErrorKind::NotConnected);
7979
assert_eq!("Dial error: no addresses for peer.", e.to_string());
8080
}
81+
82+
#[tokio::test]
83+
async fn open_stream_on_connection_targets_an_existing_connection() {
84+
let mut swarm1 = Swarm::new_ephemeral_tokio(|_| stream::Behaviour::new());
85+
let mut swarm2 = Swarm::new_ephemeral_tokio(|_| stream::Behaviour::new());
86+
87+
let mut control = swarm1.behaviour().new_control();
88+
let mut incoming = swarm2.behaviour().new_control().accept(PROTOCOL).unwrap();
89+
90+
let (addr, _) = swarm2.listen().with_memory_addr_external().await;
91+
let swarm2_peer_id = *swarm2.local_peer_id();
92+
93+
tokio::spawn(async move {
94+
while let Some((_, mut stream)) = incoming.next().await {
95+
stream.write_all(&[42]).await.unwrap();
96+
stream.close().await.unwrap();
97+
}
98+
});
99+
tokio::spawn(swarm2.loop_on_next());
100+
101+
// Dial and capture the established connection id.
102+
swarm1.dial(addr).unwrap();
103+
let conn_id = loop {
104+
if let SwarmEvent::ConnectionEstablished { connection_id, .. } =
105+
swarm1.next_swarm_event().await
106+
{
107+
break connection_id;
108+
}
109+
};
110+
tokio::spawn(swarm1.loop_on_next());
111+
112+
let mut stream = control
113+
.open_stream_on_connection(swarm2_peer_id, conn_id, PROTOCOL)
114+
.await
115+
.unwrap();
116+
let mut buf = [0u8; 1];
117+
stream.read_exact(&mut buf).await.unwrap();
118+
assert_eq!([42], buf);
119+
}
120+
121+
#[tokio::test]
122+
async fn open_stream_on_connection_errors_on_unknown_connection() {
123+
let mut swarm1 = Swarm::new_ephemeral_tokio(|_| stream::Behaviour::new());
124+
let mut swarm2 = Swarm::new_ephemeral_tokio(|_| stream::Behaviour::new());
125+
126+
let mut control = swarm1.behaviour().new_control();
127+
swarm2.listen().with_memory_addr_external().await;
128+
swarm1.connect(&mut swarm2).await;
129+
let swarm2_peer_id = *swarm2.local_peer_id();
130+
131+
tokio::spawn(swarm1.loop_on_next());
132+
tokio::spawn(swarm2.loop_on_next());
133+
134+
let bogus = ConnectionId::new_unchecked(99999);
135+
let error = control
136+
.open_stream_on_connection(swarm2_peer_id, bogus, PROTOCOL)
137+
.await
138+
.unwrap_err();
139+
let OpenStreamError::Io(e) = error else {
140+
panic!("unexpected error: {error}")
141+
};
142+
assert_eq!(e.kind(), io::ErrorKind::NotConnected);
143+
}

0 commit comments

Comments
 (0)