Skip to content

Commit 41da782

Browse files
fix(relay): correctly track associated state of inflight streams
In the relay client, we need to track a fair bit of data _alongside_ `Future`s that are executing within a `FuturesSet`. In particular, we need to track the channels that connect us the `Transport`. Using `?` within the actual `async` block of the protocol is crucial for ergonomics but essentially locks us out of passing the channel _into_ the `Future` itself. Hence, we need to track it outside. As we can see from #4822, doing so in a separate list is error prone. We solve this by introducing the `FuturesTupleSet` type to `futures-bounded`. This is a variation of `FuturesSet` that carries a piece of arbitrary data alongside the `Future` that is executing and returns it back upon completion. Using this within the relay code reveals another bug where we mistakenly confused a timed out `CONNECT` request for a timed out `RESERVE` request. Fixes: #4822. Pull-Request: #4841.
1 parent e3db5a5 commit 41da782

File tree

8 files changed

+147
-87
lines changed

8 files changed

+147
-87
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ rust-version = "1.73.0"
7171

7272
[workspace.dependencies]
7373
asynchronous-codec = { version = "0.7.0" }
74-
futures-bounded = { version = "0.2.2", path = "misc/futures-bounded" }
74+
futures-bounded = { version = "0.2.3", path = "misc/futures-bounded" }
7575
libp2p = { version = "0.53.0", path = "libp2p" }
7676
libp2p-allow-block-list = { version = "0.3.0", path = "misc/allow-block-list" }
7777
libp2p-autonat = { version = "0.12.0", path = "protocols/autonat" }

misc/futures-bounded/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.2.3 - unreleased
2+
3+
- Introduce `FuturesTupleSet`, holding tuples of a `Future` together with an arbitrary piece of data.
4+
See [PR 4841](https://github.com/libp2p/rust-lib2pp/pulls/4841).
5+
16
## 0.2.2
27

38
- Fix an issue where `{Futures,Stream}Map` returns `Poll::Pending` despite being ready after an item has been replaced as part of `try_push`.

misc/futures-bounded/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "futures-bounded"
3-
version = "0.2.2"
3+
version = "0.2.3"
44
edition = "2021"
55
rust-version.workspace = true
66
license = "MIT"
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use std::collections::HashMap;
2+
use std::future::Future;
3+
use std::task::{ready, Context, Poll};
4+
use std::time::Duration;
5+
6+
use futures_util::future::BoxFuture;
7+
8+
use crate::{FuturesMap, PushError, Timeout};
9+
10+
/// Represents a list of tuples of a [Future] and an associated piece of data.
11+
///
12+
/// Each future must finish within the specified time and the list never outgrows its capacity.
13+
pub struct FuturesTupleSet<O, D> {
14+
id: u32,
15+
inner: FuturesMap<u32, O>,
16+
data: HashMap<u32, D>,
17+
}
18+
19+
impl<O, D> FuturesTupleSet<O, D> {
20+
pub fn new(timeout: Duration, capacity: usize) -> Self {
21+
Self {
22+
id: 0,
23+
inner: FuturesMap::new(timeout, capacity),
24+
data: HashMap::new(),
25+
}
26+
}
27+
}
28+
29+
impl<O, D> FuturesTupleSet<O, D>
30+
where
31+
O: 'static,
32+
{
33+
/// Push a future into the list.
34+
///
35+
/// This method adds the given future to the list.
36+
/// If the length of the list is equal to the capacity, this method returns a error that contains the passed future.
37+
/// In that case, the future is not added to the set.
38+
pub fn try_push<F>(&mut self, future: F, data: D) -> Result<(), (BoxFuture<O>, D)>
39+
where
40+
F: Future<Output = O> + Send + 'static,
41+
{
42+
self.id = self.id.wrapping_add(1);
43+
44+
match self.inner.try_push(self.id, future) {
45+
Ok(()) => {}
46+
Err(PushError::BeyondCapacity(w)) => return Err((w, data)),
47+
Err(PushError::Replaced(_)) => unreachable!("we never reuse IDs"),
48+
}
49+
self.data.insert(self.id, data);
50+
51+
Ok(())
52+
}
53+
54+
pub fn len(&self) -> usize {
55+
self.inner.len()
56+
}
57+
58+
pub fn is_empty(&self) -> bool {
59+
self.inner.is_empty()
60+
}
61+
62+
pub fn poll_ready_unpin(&mut self, cx: &mut Context<'_>) -> Poll<()> {
63+
self.inner.poll_ready_unpin(cx)
64+
}
65+
66+
pub fn poll_unpin(&mut self, cx: &mut Context<'_>) -> Poll<(Result<O, Timeout>, D)> {
67+
let (id, res) = ready!(self.inner.poll_unpin(cx));
68+
let data = self.data.remove(&id).expect("must have data for future");
69+
70+
Poll::Ready((res, data))
71+
}
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
use futures_util::future::poll_fn;
78+
use futures_util::FutureExt;
79+
use std::future::ready;
80+
81+
#[test]
82+
fn tracks_associated_data_of_future() {
83+
let mut set = FuturesTupleSet::new(Duration::from_secs(10), 10);
84+
85+
let _ = set.try_push(ready(1), 1);
86+
let _ = set.try_push(ready(2), 2);
87+
88+
let (res1, data1) = poll_fn(|cx| set.poll_unpin(cx)).now_or_never().unwrap();
89+
let (res2, data2) = poll_fn(|cx| set.poll_unpin(cx)).now_or_never().unwrap();
90+
91+
assert_eq!(res1.unwrap(), data1);
92+
assert_eq!(res2.unwrap(), data2);
93+
}
94+
}

misc/futures-bounded/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
mod futures_map;
22
mod futures_set;
3+
mod futures_tuple_set;
34
mod stream_map;
45
mod stream_set;
56

67
pub use futures_map::FuturesMap;
78
pub use futures_set::FuturesSet;
9+
pub use futures_tuple_set::FuturesTupleSet;
810
pub use stream_map::StreamMap;
911
pub use stream_set::StreamSet;
1012

protocols/relay/CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
21
## 0.17.1 - unreleased
2+
33
- Automatically register relayed addresses as external addresses.
44
See [PR 4809](https://github.com/libp2p/rust-lib2pp/pulls/4809).
5+
- Fix an error where performing too many reservations at once could lead to inconsistent internal state.
6+
See [PR 4841](https://github.com/libp2p/rust-libp2p/pull/4841).
57

68
## 0.17.0
79
- Don't close connections on protocol failures within the relay-server.

protocols/relay/src/priv_client/handler.rs

Lines changed: 40 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,15 @@ pub struct Handler {
107107
/// We issue a stream upgrade for each pending request.
108108
pending_requests: VecDeque<PendingRequest>,
109109

110-
/// A `RESERVE` request is in-flight for each item in this queue.
111-
active_reserve_requests: VecDeque<mpsc::Sender<transport::ToListenerMsg>>,
112-
113-
inflight_reserve_requests:
114-
futures_bounded::FuturesSet<Result<outbound_hop::Reservation, outbound_hop::ReserveError>>,
115-
116-
/// A `CONNECT` request is in-flight for each item in this queue.
117-
active_connect_requests:
118-
VecDeque<oneshot::Sender<Result<priv_client::Connection, outbound_hop::ConnectError>>>,
110+
inflight_reserve_requests: futures_bounded::FuturesTupleSet<
111+
Result<outbound_hop::Reservation, outbound_hop::ReserveError>,
112+
mpsc::Sender<transport::ToListenerMsg>,
113+
>,
119114

120-
inflight_outbound_connect_requests:
121-
futures_bounded::FuturesSet<Result<outbound_hop::Circuit, outbound_hop::ConnectError>>,
115+
inflight_outbound_connect_requests: futures_bounded::FuturesTupleSet<
116+
Result<outbound_hop::Circuit, outbound_hop::ConnectError>,
117+
oneshot::Sender<Result<priv_client::Connection, outbound_hop::ConnectError>>,
118+
>,
122119

123120
inflight_inbound_circuit_requests:
124121
futures_bounded::FuturesSet<Result<inbound_stop::Circuit, inbound_stop::Error>>,
@@ -137,24 +134,22 @@ impl Handler {
137134
remote_addr,
138135
queued_events: Default::default(),
139136
pending_requests: Default::default(),
140-
active_reserve_requests: Default::default(),
141-
inflight_reserve_requests: futures_bounded::FuturesSet::new(
137+
inflight_reserve_requests: futures_bounded::FuturesTupleSet::new(
142138
STREAM_TIMEOUT,
143139
MAX_CONCURRENT_STREAMS_PER_CONNECTION,
144140
),
145141
inflight_inbound_circuit_requests: futures_bounded::FuturesSet::new(
146142
STREAM_TIMEOUT,
147143
MAX_CONCURRENT_STREAMS_PER_CONNECTION,
148144
),
149-
inflight_outbound_connect_requests: futures_bounded::FuturesSet::new(
145+
inflight_outbound_connect_requests: futures_bounded::FuturesTupleSet::new(
150146
STREAM_TIMEOUT,
151147
MAX_CONCURRENT_STREAMS_PER_CONNECTION,
152148
),
153149
inflight_outbound_circuit_deny_requests: futures_bounded::FuturesSet::new(
154150
DENYING_CIRCUIT_TIMEOUT,
155151
MAX_NUMBER_DENYING_CIRCUIT,
156152
),
157-
active_connect_requests: Default::default(),
158153
reservation: Reservation::None,
159154
}
160155
}
@@ -276,24 +271,16 @@ impl ConnectionHandler for Handler {
276271
ConnectionHandlerEvent<Self::OutboundProtocol, Self::OutboundOpenInfo, Self::ToBehaviour>,
277272
> {
278273
loop {
279-
debug_assert_eq!(
280-
self.inflight_reserve_requests.len(),
281-
self.active_reserve_requests.len(),
282-
"expect to have one active request per inflight stream"
283-
);
284-
285274
// Reservations
286275
match self.inflight_reserve_requests.poll_unpin(cx) {
287-
Poll::Ready(Ok(Ok(outbound_hop::Reservation {
288-
renewal_timeout,
289-
addrs,
290-
limit,
291-
}))) => {
292-
let to_listener = self
293-
.active_reserve_requests
294-
.pop_front()
295-
.expect("must have active request for stream");
296-
276+
Poll::Ready((
277+
Ok(Ok(outbound_hop::Reservation {
278+
renewal_timeout,
279+
addrs,
280+
limit,
281+
})),
282+
to_listener,
283+
)) => {
297284
return Poll::Ready(ConnectionHandlerEvent::NotifyBehaviour(
298285
self.reservation.accepted(
299286
renewal_timeout,
@@ -304,12 +291,7 @@ impl ConnectionHandler for Handler {
304291
),
305292
));
306293
}
307-
Poll::Ready(Ok(Err(error))) => {
308-
let mut to_listener = self
309-
.active_reserve_requests
310-
.pop_front()
311-
.expect("must have active request for stream");
312-
294+
Poll::Ready((Ok(Err(error)), mut to_listener)) => {
313295
if let Err(e) =
314296
to_listener.try_send(transport::ToListenerMsg::Reservation(Err(error)))
315297
{
@@ -318,12 +300,7 @@ impl ConnectionHandler for Handler {
318300
self.reservation.failed();
319301
continue;
320302
}
321-
Poll::Ready(Err(futures_bounded::Timeout { .. })) => {
322-
let mut to_listener = self
323-
.active_reserve_requests
324-
.pop_front()
325-
.expect("must have active request for stream");
326-
303+
Poll::Ready((Err(futures_bounded::Timeout { .. }), mut to_listener)) => {
327304
if let Err(e) =
328305
to_listener.try_send(transport::ToListenerMsg::Reservation(Err(
329306
outbound_hop::ReserveError::Io(io::ErrorKind::TimedOut.into()),
@@ -337,25 +314,17 @@ impl ConnectionHandler for Handler {
337314
Poll::Pending => {}
338315
}
339316

340-
debug_assert_eq!(
341-
self.inflight_outbound_connect_requests.len(),
342-
self.active_connect_requests.len(),
343-
"expect to have one active request per inflight stream"
344-
);
345-
346317
// Circuits
347318
match self.inflight_outbound_connect_requests.poll_unpin(cx) {
348-
Poll::Ready(Ok(Ok(outbound_hop::Circuit {
349-
limit,
350-
read_buffer,
351-
stream,
352-
}))) => {
353-
let to_listener = self
354-
.active_connect_requests
355-
.pop_front()
356-
.expect("must have active request for stream");
357-
358-
if to_listener
319+
Poll::Ready((
320+
Ok(Ok(outbound_hop::Circuit {
321+
limit,
322+
read_buffer,
323+
stream,
324+
})),
325+
to_dialer,
326+
)) => {
327+
if to_dialer
359328
.send(Ok(priv_client::Connection {
360329
state: priv_client::ConnectionState::new_outbound(stream, read_buffer),
361330
}))
@@ -371,27 +340,18 @@ impl ConnectionHandler for Handler {
371340
Event::OutboundCircuitEstablished { limit },
372341
));
373342
}
374-
Poll::Ready(Ok(Err(error))) => {
375-
let to_dialer = self
376-
.active_connect_requests
377-
.pop_front()
378-
.expect("must have active request for stream");
379-
343+
Poll::Ready((Ok(Err(error)), to_dialer)) => {
380344
let _ = to_dialer.send(Err(error));
381345
continue;
382346
}
383-
Poll::Ready(Err(futures_bounded::Timeout { .. })) => {
384-
let mut to_listener = self
385-
.active_reserve_requests
386-
.pop_front()
387-
.expect("must have active request for stream");
388-
389-
if let Err(e) =
390-
to_listener.try_send(transport::ToListenerMsg::Reservation(Err(
391-
outbound_hop::ReserveError::Io(io::ErrorKind::TimedOut.into()),
347+
Poll::Ready((Err(futures_bounded::Timeout { .. }), to_dialer)) => {
348+
if to_dialer
349+
.send(Err(outbound_hop::ConnectError::Io(
350+
io::ErrorKind::TimedOut.into(),
392351
)))
352+
.is_err()
393353
{
394-
tracing::debug!("Unable to send error to listener: {}", e.into_send_error())
354+
tracing::debug!("Unable to send error to dialer")
395355
}
396356
self.reservation.failed();
397357
continue;
@@ -499,27 +459,24 @@ impl ConnectionHandler for Handler {
499459
);
500460
match pending_request {
501461
PendingRequest::Reserve { to_listener } => {
502-
self.active_reserve_requests.push_back(to_listener);
503462
if self
504463
.inflight_reserve_requests
505-
.try_push(outbound_hop::make_reservation(stream))
464+
.try_push(outbound_hop::make_reservation(stream), to_listener)
506465
.is_err()
507466
{
508-
tracing::warn!("Dropping outbound stream because we are at capacity")
467+
tracing::warn!("Dropping outbound stream because we are at capacity");
509468
}
510469
}
511470
PendingRequest::Connect {
512471
dst_peer_id,
513472
to_dial: send_back,
514473
} => {
515-
self.active_connect_requests.push_back(send_back);
516-
517474
if self
518475
.inflight_outbound_connect_requests
519-
.try_push(outbound_hop::open_circuit(stream, dst_peer_id))
476+
.try_push(outbound_hop::open_circuit(stream, dst_peer_id), send_back)
520477
.is_err()
521478
{
522-
tracing::warn!("Dropping outbound stream because we are at capacity")
479+
tracing::warn!("Dropping outbound stream because we are at capacity");
523480
}
524481
}
525482
}

0 commit comments

Comments
 (0)