Skip to content

Commit f9928ec

Browse files
committed
fix: improve the error space and add the possibility of graceful shutdown
1 parent c191338 commit f9928ec

File tree

8 files changed

+278
-92
lines changed

8 files changed

+278
-92
lines changed

rs/p2p/consensus_manager/src/sender.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -536,11 +536,7 @@ mod tests {
536536
mock_transport
537537
.expect_push()
538538
.times(5)
539-
.returning(move |_, _| {
540-
Err(SendError::ConnectionNotFound {
541-
reason: String::new(),
542-
})
543-
})
539+
.returning(move |_, _| Err(SendError::ConnectionUnavailable(String::new())))
544540
.in_sequence(&mut seq);
545541
mock_transport
546542
.expect_push()

rs/p2p/memory_transport/src/lib.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,9 @@ impl Transport for PeerTransport {
282282
mut request: Request<Bytes>,
283283
) -> Result<Response<Bytes>, SendError> {
284284
if peer_id == &self.node_id {
285-
panic!("Should not happen");
285+
return Err(SendError::ConnectionUnavailable(
286+
"Can't connect to self".to_string(),
287+
));
286288
}
287289

288290
let (oneshot_tx, oneshot_rx) = oneshot::channel();
@@ -292,15 +294,15 @@ impl Transport for PeerTransport {
292294
.send((request, *peer_id, oneshot_tx))
293295
.is_err()
294296
{
295-
return Err(SendError::SendRequestFailed {
296-
reason: String::from("router channel closed"),
297-
});
297+
return Err(SendError::ConnectionUnavailable(String::from(
298+
"router channel closed",
299+
)));
298300
}
299301
match oneshot_rx.await {
300302
Ok(r) => Ok(r),
301-
Err(_) => Err(SendError::RecvResponseFailed {
302-
reason: "channel closed".to_owned(),
303-
}),
303+
Err(_) => Err(SendError::ConnectionUnavailable(String::from(
304+
"channel closed",
305+
))),
304306
}
305307
}
306308

rs/p2p/quic_transport/README.adoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,22 @@ A nice writeup about async and blocking operations can be found in https://ryhl.
6868
1. Use QUIC to statisfy the first two requirements ("Reliable data delivery" and "Multiplexing").
6969
2. Open new QUIC stream for each message, instead of one stream per handler/URI. TODO: explain.
7070
3. Handlers are always ready to process messages, hence the router is always ready to accept streams. TODO: explain.
71+
72+
=== Breaking dependency cycles in P2P protocols ===
73+
74+
In any design that uses a single connection for inbound and outbound traffic with
75+
designated listner and dialer there exists a circlular dependency between the read and write paths.
76+
There are different approaches of breaking this depedency.
77+
78+
The IC P2P protocols have a single event loop per P2P protocol that drives the outbound traffic.
79+
This follows the libp2p philosophy.
80+
Hence only those event loops need access to the `QuicTransport` object.
81+
In this model handlers can have a channel to the main event loop.
82+
83+
Alternative approach (the one that SUI takes) is to have weak references
84+
to the transport object that can be used directly in the handlers. As a result,
85+
when there are handlers that take the weak reference the transport object needs to first be instantiated
86+
and later started with the already constructed router.
87+
This approach enables the handlers for doing most of the work and potentially eliminating the need
88+
of the event loop from the first approach.
89+
However, this comes at the cost of having more shared state and more contention.

rs/p2p/quic_transport/src/connection_handle.rs

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,41 +66,37 @@ impl ConnectionHandle {
6666
// Propagate PeerId from this connection to lower layers.
6767
request.extensions_mut().insert(self.peer_id);
6868

69-
let (mut send_stream, recv_stream) = self.connection.open_bi().await.map_err(|e| {
69+
let (mut send_stream, recv_stream) = self.connection.open_bi().await.map_err(|err| {
7070
self.metrics
7171
.connection_handle_errors_total
7272
.with_label_values(&[REQUEST_TYPE_RPC, ERROR_TYPE_OPEN]);
73-
SendError::SendRequestFailed {
74-
reason: e.to_string(),
75-
}
73+
err
7674
})?;
7775

7876
write_request(&mut send_stream, request)
7977
.await
80-
.map_err(|e| {
78+
.map_err(|err| {
8179
self.metrics
8280
.connection_handle_errors_total
8381
.with_label_values(&[REQUEST_TYPE_RPC, ERROR_TYPE_WRITE])
8482
.inc();
85-
SendError::SendRequestFailed { reason: e }
83+
err
8684
})?;
8785

88-
send_stream.finish().await.map_err(|e| {
86+
send_stream.finish().await.map_err(|err| {
8987
self.metrics
9088
.connection_handle_errors_total
9189
.with_label_values(&[REQUEST_TYPE_RPC, ERROR_TYPE_FINISH])
9290
.inc();
93-
SendError::SendRequestFailed {
94-
reason: e.to_string(),
95-
}
91+
err
9692
})?;
9793

98-
let mut response = read_response(recv_stream).await.map_err(|e| {
94+
let mut response = read_response(recv_stream).await.map_err(|err| {
9995
self.metrics
10096
.connection_handle_errors_total
10197
.with_label_values(&[REQUEST_TYPE_RPC, ERROR_TYPE_READ])
10298
.inc();
103-
SendError::RecvResponseFailed { reason: e }
99+
err
104100
})?;
105101

106102
// Propagate PeerId from this request to upper layers.
@@ -124,33 +120,29 @@ impl ConnectionHandle {
124120
// Propagate PeerId from this connection to lower layers.
125121
request.extensions_mut().insert(self.peer_id);
126122

127-
let mut send_stream = self.connection.open_uni().await.map_err(|e| {
123+
let mut send_stream = self.connection.open_uni().await.map_err(|err| {
128124
self.metrics
129125
.connection_handle_errors_total
130126
.with_label_values(&[REQUEST_TYPE_PUSH, ERROR_TYPE_OPEN]);
131-
SendError::SendRequestFailed {
132-
reason: e.to_string(),
133-
}
127+
err
134128
})?;
135129

136130
write_request(&mut send_stream, request)
137131
.await
138-
.map_err(|e| {
132+
.map_err(|err| {
139133
self.metrics
140134
.connection_handle_errors_total
141135
.with_label_values(&[REQUEST_TYPE_PUSH, ERROR_TYPE_WRITE])
142136
.inc();
143-
SendError::SendRequestFailed { reason: e }
137+
err
144138
})?;
145139

146-
send_stream.finish().await.map_err(|e| {
140+
send_stream.finish().await.map_err(|err| {
147141
self.metrics
148142
.connection_handle_errors_total
149143
.with_label_values(&[REQUEST_TYPE_PUSH, ERROR_TYPE_FINISH])
150144
.inc();
151-
SendError::SendRequestFailed {
152-
reason: e.to_string(),
153-
}
145+
err
154146
})?;
155147

156148
Ok(())

rs/p2p/quic_transport/src/connection_manager.rs

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,13 @@ use quinn::{
5656
EndpointConfig, RecvStream, SendStream, VarInt,
5757
};
5858
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
59+
use thiserror::Error;
5960
use tokio::{
6061
io::{AsyncRead, AsyncWrite},
6162
runtime::Handle,
6263
select,
64+
sync::mpsc::{channel, Receiver, Sender},
65+
sync::oneshot,
6366
task::JoinSet,
6467
};
6568
use tokio_util::time::DelayQueue;
@@ -122,6 +125,8 @@ struct ConnectionManager {
122125
/// JoinMap that stores active connection handlers keyed by peer id.
123126
active_connections: JoinMap<NodeId, ()>,
124127

128+
/// The channel is used for graceful shutdown of the connection manager.
129+
shutdown_rx: Receiver<oneshot::Sender<()>>,
125130
/// Endpoint config
126131
endpoint: Endpoint,
127132
transport_config: Arc<quinn::TransportConfig>,
@@ -188,6 +193,24 @@ impl std::fmt::Display for ConnectionEstablishError {
188193
}
189194
}
190195

196+
#[derive(Error, Debug)]
197+
#[error("ShutdownError")]
198+
pub(crate) struct ShutdownError;
199+
pub(crate) struct ShutdownHandle(Sender<oneshot::Sender<()>>);
200+
201+
impl ShutdownHandle {
202+
fn new() -> (Self, Receiver<oneshot::Sender<()>>) {
203+
let (shutdown_tx, shutdown_rx) = channel(10);
204+
(ShutdownHandle(shutdown_tx), shutdown_rx)
205+
}
206+
207+
pub(crate) async fn shutdown(&mut self) -> Result<(), ShutdownError> {
208+
let (wait_tx, wait_rx) = oneshot::channel();
209+
self.0.send(wait_tx).await.map_err(|_| ShutdownError)?;
210+
wait_rx.await.map_err(|_| ShutdownError)
211+
}
212+
}
213+
191214
pub(crate) fn start_connection_manager(
192215
log: &ReplicaLogger,
193216
metrics_registry: &MetricsRegistry,
@@ -200,7 +223,7 @@ pub(crate) fn start_connection_manager(
200223
watcher: tokio::sync::watch::Receiver<SubnetTopology>,
201224
socket: Either<SocketAddr, impl AsyncUdpSocket>,
202225
router: Router,
203-
) {
226+
) -> ShutdownHandle {
204227
let topology = watcher.borrow().clone();
205228

206229
let metrics = QuicTransportMetrics::new(metrics_registry);
@@ -294,6 +317,8 @@ pub(crate) fn start_connection_manager(
294317
.expect("Failed to create endpoint"),
295318
};
296319

320+
let (shutdown_handler, shutdown_rx) = ShutdownHandle::new();
321+
297322
let manager = ConnectionManager {
298323
log: log.clone(),
299324
rt: rt.clone(),
@@ -311,10 +336,12 @@ pub(crate) fn start_connection_manager(
311336
outbound_connecting: JoinMap::new(),
312337
inbound_connecting: JoinSet::new(),
313338
active_connections: JoinMap::new(),
339+
shutdown_rx,
314340
router,
315341
};
316342

317343
rt.spawn(manager.run());
344+
shutdown_handler
318345
}
319346

320347
impl ConnectionManager {
@@ -323,8 +350,11 @@ impl ConnectionManager {
323350
}
324351

325352
pub async fn run(mut self) {
326-
loop {
353+
let shutdown_notifier = loop {
327354
select! {
355+
shutdown_notifier = self.shutdown_rx.recv() => {
356+
break shutdown_notifier;
357+
}
328358
Some(reconnect) = self.connect_queue.next() => {
329359
self.handle_dial(reconnect.into_inner())
330360
}
@@ -334,18 +364,23 @@ impl ConnectionManager {
334364
self.handle_topology_change();
335365
},
336366
Err(_) => {
337-
error!(self.log, "Transport disconnected from peer manager. Shutting down.");
338-
break
367+
// If this happens it means that peer discovery is not working anymore.
368+
// There are few options in this case
369+
// 1. continue working with the existing set of connections (preferred)
370+
// 2. panic
371+
// 3. do a graceful shutdown and rely on clients of transport to handle this fallout
372+
// (least preferred because this is not recoverable error)
373+
error!(self.log, "Transport disconnected from peer manager.");
339374
}
340375
}
341376
},
342377
connecting = self.endpoint.accept() => {
343378
if let Some(connecting) = connecting {
344379
self.handle_inbound(connecting);
345380
} else {
346-
info!(self.log, "Quic endpoint closed. Stopping transport.");
347-
// Endpoint is closed. This indicates a shutdown.
348-
break;
381+
error!(self.log, "Quic endpoint closed. Stopping transport.");
382+
// Endpoint is closed. This indicates NOT graceful shutdown.
383+
break None;
349384
}
350385
},
351386
Some(conn_res) = self.outbound_connecting.join_next() => {
@@ -392,13 +427,23 @@ impl ConnectionManager {
392427
self.metrics
393428
.delay_queue_size
394429
.set(self.connect_queue.len() as i64);
395-
}
396-
// This point is reached only in two cases - replica gracefully shutting down or
397-
// bug which makes the peer manager unavailable.
398-
// If the peer manager is unavailable, the replica needs must exist that's why
399-
// the endpoint is closed proactively.
400-
self.endpoint.close(0u8.into(), b"shutting down");
430+
};
431+
self.shutdown().await;
432+
// The send can fail iff the shutdown handler was dropped already.
433+
let _ = shutdown_notifier
434+
.expect("Transport 't stop unexpectedly. This is serious unrecoverable bug. It is better to crash.")
435+
.send(());
436+
}
401437

438+
// TODO: maybe unbind the port so we can start another transport on the same port after shutdown.
439+
async fn shutdown(mut self) {
440+
self.peer_map.write().unwrap().clear();
441+
self.endpoint
442+
.close(VarInt::from_u32(0), b"graceful shutdown of endpoint");
443+
self.connect_queue.clear();
444+
self.inbound_connecting.shutdown().await;
445+
self.outbound_connecting.shutdown().await;
446+
self.active_connections.shutdown().await;
402447
self.endpoint.wait_idle().await;
403448
}
404449

0 commit comments

Comments
 (0)