Skip to content

Commit 669dccd

Browse files
authored
Merge pull request #4141 from ProvableHQ/feat/low_level_tweaks
[Feat] Low-level network tweaks
2 parents 3eeaadd + c293449 commit 669dccd

11 files changed

Lines changed: 175 additions & 71 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,7 +819,7 @@ workflows:
819819
filters:
820820
branches:
821821
only:
822-
- feat/vk-migration
822+
- fix/sync-race-conditions
823823
- canary
824824
- testnet
825825
- mainnet

node/router/src/heartbeat.rs

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use crate::{
1919
NodeType,
2020
Outbound,
2121
PeerPoolHandling,
22-
Router,
2322
bootstrap_peers,
2423
messages::{DisconnectReason, Message, PeerRequest},
2524
};
@@ -67,8 +66,6 @@ pub trait Heartbeat<N: Network>: Outbound<N> {
6766
self.safety_check_minimum_number_of_peers();
6867
self.log_connected_peers();
6968

70-
// Remove any stale connected peers.
71-
self.remove_stale_connected_peers();
7269
// Remove the oldest connected peer.
7370
self.remove_oldest_connected_peer();
7471
// Keep the number of connected peers within the allowed range.
@@ -111,20 +108,6 @@ pub trait Heartbeat<N: Network>: Outbound<N> {
111108
}
112109
}
113110

114-
/// This function removes any connected peers that have not communicated within the predefined time.
115-
fn remove_stale_connected_peers(&self) {
116-
// Check if any connected peer is stale.
117-
for peer in self.router().get_connected_peers() {
118-
// Disconnect if the peer has not communicated back within the predefined time.
119-
let elapsed = peer.last_seen.elapsed();
120-
if elapsed > Router::<N>::MAX_RADIO_SILENCE {
121-
warn!("Peer '{}' has not communicated in {elapsed:?}", peer.listener_addr);
122-
// Disconnect from this peer.
123-
self.router().disconnect(peer.listener_addr);
124-
}
125-
}
126-
}
127-
128111
/// Returns a sorted vector of network addresses of all removable connected peers
129112
/// where the first entry has the lowest priority and the last one the highest.
130113
///

node/router/src/lib.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ use anyhow::Result;
6969
use locktick::parking_lot::{Mutex, RwLock};
7070
#[cfg(not(feature = "locktick"))]
7171
use parking_lot::{Mutex, RwLock};
72-
use std::{collections::HashMap, future::Future, io, net::SocketAddr, ops::Deref, sync::Arc, time::Duration};
72+
use std::{collections::HashMap, future::Future, io, net::SocketAddr, ops::Deref, sync::Arc};
7373
use tokio::task::JoinHandle;
7474

7575
/// The default port used by the router.
@@ -147,9 +147,6 @@ impl<N: Network> Router<N> {
147147
/// The maximum amount of connection attempts within a 10 second threshold.
148148
#[cfg(not(feature = "test"))]
149149
const MAX_CONNECTION_ATTEMPTS: usize = 10;
150-
/// The duration after which a connected peer is considered inactive or
151-
/// disconnected if no message has been received in the meantime.
152-
const MAX_RADIO_SILENCE: Duration = Duration::from_secs(150); // 2.5 minutes
153150
}
154151

155152
impl<N: Network> Router<N> {

node/tcp/src/helpers/connections.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,11 @@ impl Not for ConnectionSide {
137137
}
138138
}
139139
}
140+
141+
impl Drop for Connection {
142+
fn drop(&mut self) {
143+
for task in self.tasks.iter().rev() {
144+
task.abort();
145+
}
146+
}
147+
}

node/tcp/src/protocols/disconnect.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16-
use std::net::SocketAddr;
16+
use std::{net::SocketAddr, time::Duration};
1717

18-
use tokio::sync::{mpsc, oneshot};
18+
use tokio::{
19+
sync::{mpsc, oneshot},
20+
task::JoinHandle,
21+
time::timeout,
22+
};
1923
use tracing::*;
2024

2125
#[cfg(doc)]
@@ -31,10 +35,18 @@ pub trait Disconnect: P2P
3135
where
3236
Self: Clone + Send + Sync + 'static,
3337
{
38+
/// The maximum time allowed for the on_disconnect hook to execute.
39+
/// If the hook exceeds this time, it will be aborted to ensure the node cleans up
40+
/// resources promptly.
41+
const TIMEOUT: Duration = Duration::from_secs(3);
42+
3443
/// Attaches the behavior specified in [`Disconnect::handle_disconnect`] to every occurrence of the
3544
/// node disconnecting from a peer.
3645
async fn enable_disconnect(&self) {
37-
let (from_node_sender, mut from_node_receiver) = mpsc::unbounded_channel::<(SocketAddr, oneshot::Sender<()>)>();
46+
let (from_node_sender, mut from_node_receiver) = mpsc::channel::<(
47+
SocketAddr,
48+
oneshot::Sender<(JoinHandle<()>, oneshot::Receiver<()>)>,
49+
)>(self.tcp().config().max_connections as usize);
3850

3951
// use a channel to know when the disconnect task is ready
4052
let (tx, rx) = oneshot::channel::<()>();
@@ -47,13 +59,20 @@ where
4759

4860
while let Some((peer_addr, notifier)) = from_node_receiver.recv().await {
4961
let self_clone2 = self_clone.clone();
50-
tokio::spawn(async move {
62+
// create a channel for waiting on completion
63+
let (done_tx, done_rx) = oneshot::channel();
64+
let handle = tokio::spawn(async move {
5165
// perform the specified extra actions
52-
self_clone2.handle_disconnect(peer_addr).await;
66+
if timeout(Self::TIMEOUT, self_clone2.handle_disconnect(peer_addr)).await.is_err() {
67+
warn!(parent: self_clone2.tcp().span(), "Disconnect logic timed out for {peer_addr}");
68+
}
5369
// notify the node that the extra actions have concluded
5470
// and that the related connection can be dropped
55-
let _ = notifier.send(()); // can't really fail
71+
let _ = done_tx.send(());
5672
});
73+
// provide the node with a handle to the scheduled task,
74+
// and a receiver that will notify it of its completion
75+
let _ = notifier.send((handle, done_rx)); // can't really fail
5776
}
5877
});
5978
let _ = rx.await;

node/tcp/src/protocols/handshake.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ where
4545

4646
/// Prepares the node to perform specified network handshakes.
4747
async fn enable_handshake(&self) {
48-
let (from_node_sender, mut from_node_receiver) = mpsc::unbounded_channel::<ReturnableConnection>();
48+
let (from_node_sender, mut from_node_receiver) =
49+
mpsc::channel::<ReturnableConnection>(self.tcp().config().max_connections as usize);
4950

5051
// use a channel to know when the handshake task is ready
5152
let (tx, rx) = oneshot::channel();

node/tcp/src/protocols/mod.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
use std::{io, net::SocketAddr};
2121

2222
use once_cell::race::OnceBox;
23-
use tokio::sync::{mpsc, oneshot};
23+
use tokio::{
24+
sync::{mpsc, oneshot},
25+
task::JoinHandle,
26+
};
2427

2528
use crate::connections::Connection;
2629

@@ -36,13 +39,17 @@ pub use on_connect::OnConnect;
3639
pub use reading::Reading;
3740
pub use writing::Writing;
3841

42+
// The value returned to the node by the OnDisconnect protocol is a bit complex,
43+
// so use an alias to break it down.
44+
type OnDisconnectBundle = (JoinHandle<()>, oneshot::Receiver<()>);
45+
3946
#[derive(Default)]
4047
pub(crate) struct Protocols {
4148
pub(crate) handshake: OnceBox<ProtocolHandler<Connection, io::Result<Connection>>>,
4249
pub(crate) reading: OnceBox<ProtocolHandler<Connection, io::Result<Connection>>>,
4350
pub(crate) writing: OnceBox<writing::WritingHandler>,
44-
pub(crate) on_connect: OnceBox<ProtocolHandler<SocketAddr, ()>>,
45-
pub(crate) disconnect: OnceBox<ProtocolHandler<SocketAddr, ()>>,
51+
pub(crate) on_connect: OnceBox<ProtocolHandler<SocketAddr, JoinHandle<()>>>,
52+
pub(crate) disconnect: OnceBox<ProtocolHandler<SocketAddr, OnDisconnectBundle>>,
4653
}
4754

4855
/// An object sent to a protocol handler task; the task assumes control of a protocol-relevant item `T`,
@@ -52,15 +59,15 @@ pub(crate) type ReturnableItem<T, U> = (T, oneshot::Sender<U>);
5259

5360
pub(crate) type ReturnableConnection = ReturnableItem<Connection, io::Result<Connection>>;
5461

55-
pub(crate) struct ProtocolHandler<T, U>(mpsc::UnboundedSender<ReturnableItem<T, U>>);
62+
pub(crate) struct ProtocolHandler<T, U>(mpsc::Sender<ReturnableItem<T, U>>);
5663

5764
pub(crate) trait Protocol<T, U> {
58-
fn trigger(&self, item: ReturnableItem<T, U>);
65+
async fn trigger(&self, item: ReturnableItem<T, U>);
5966
}
6067

6168
impl<T, U> Protocol<T, U> for ProtocolHandler<T, U> {
62-
fn trigger(&self, item: ReturnableItem<T, U>) {
69+
async fn trigger(&self, item: ReturnableItem<T, U>) {
6370
// ignore errors; they can only happen if a disconnect interrupts the protocol setup process
64-
let _ = self.0.send(item);
71+
let _ = self.0.send(item).await;
6572
}
6673
}

node/tcp/src/protocols/on_connect.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ where
3535
/// Attaches the behavior specified in [`OnConnect::on_connect`] right after every successful
3636
/// handshake.
3737
async fn enable_on_connect(&self) {
38-
let (from_node_sender, mut from_node_receiver) = mpsc::unbounded_channel::<(SocketAddr, oneshot::Sender<()>)>();
38+
let (from_node_sender, mut from_node_receiver) = mpsc::channel::<(
39+
SocketAddr,
40+
oneshot::Sender<tokio::task::JoinHandle<()>>,
41+
)>(self.tcp().config().max_connections as usize);
3942

4043
// use a channel to know when the on_connect task is ready
4144
let (tx, rx) = oneshot::channel::<()>();
@@ -52,12 +55,12 @@ where
5255

5356
while let Some((addr, notifier)) = from_node_receiver.recv().await {
5457
let self_clone2 = self_clone.clone();
55-
tokio::spawn(async move {
58+
let handle = tokio::spawn(async move {
5659
// perform the specified initial actions
5760
self_clone2.on_connect(addr).await;
58-
// notify the node that the initial actions have concluded
59-
let _ = notifier.send(()); // can't really fail
6061
});
62+
// notify the node that the initial actions have concluded
63+
let _ = notifier.send(handle); // can't really fail
6164
}
6265
});
6366
let _ = rx.await;

node/tcp/src/protocols/reading.rs

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,15 @@ use crate::{
2525
use async_trait::async_trait;
2626
use bytes::BytesMut;
2727
use futures_util::StreamExt;
28-
use std::{io, net::SocketAddr};
28+
use std::{
29+
io,
30+
net::SocketAddr,
31+
time::{Duration, Instant},
32+
};
2933
use tokio::{
3034
io::AsyncRead,
3135
sync::{mpsc, oneshot},
36+
time::timeout,
3237
};
3338
use tokio_util::codec::{Decoder, FramedRead};
3439
use tracing::*;
@@ -60,6 +65,9 @@ where
6065
/// The default value is 1024KiB.
6166
const INITIAL_BUFFER_SIZE: usize = 1024 * 1024;
6267

68+
/// The maximum time the node will wait for a new message before considering the connection dead.
69+
const IDLE_TIMEOUT: Duration = Duration::from_secs(150);
70+
6371
/// The final (deserialized) type of inbound messages.
6472
type Message: Send;
6573

@@ -68,7 +76,7 @@ where
6876

6977
/// Prepares the node to receive messages.
7078
async fn enable_reading(&self) {
71-
let (conn_sender, mut conn_receiver) = mpsc::unbounded_channel();
79+
let (conn_sender, mut conn_receiver) = mpsc::channel(self.tcp().config().max_connections as usize);
7280

7381
// use a channel to know when the reading task is ready
7482
let (tx_reading, rx_reading) = oneshot::channel();
@@ -168,27 +176,57 @@ impl<R: Reading> ReadingInternal for R {
168176
// this task gets aborted, so there is no need for a dedicated timeout
169177
let _ = rx_conn_ready.await;
170178

171-
while let Some(bytes) = framed.next().await {
172-
match bytes {
173-
Ok(msg) => {
179+
// dropped message log suppression helpers
180+
let mut dropped_count: usize = 0;
181+
let mut last_drop_log = Instant::now();
182+
183+
loop {
184+
let next_frame_future = framed.next();
185+
let read_result = match timeout(Self::IDLE_TIMEOUT, next_frame_future).await {
186+
Ok(res) => res, // IO completed (success or error)
187+
Err(_) => {
188+
debug!(parent: node.span(), "connection with {addr} timed out due to inactivity");
189+
break;
190+
}
191+
};
192+
match read_result {
193+
Some(Ok(msg)) => {
174194
// send the message for further processing
175195
if let Err(e) = inbound_message_sender.try_send(msg) {
176-
error!(parent: node.span(), "can't process a message from {addr}: {e}");
177196
node.stats().register_failure();
178-
if matches!(e, mpsc::error::TrySendError::Closed(_)) {
179-
break;
197+
match e {
198+
mpsc::error::TrySendError::Full(_) => {
199+
// avoid log flooding
200+
dropped_count += 1;
201+
if last_drop_log.elapsed() >= Duration::from_secs(1) {
202+
warn_about_dropped_messages(
203+
&node,
204+
addr,
205+
&mut dropped_count,
206+
&mut last_drop_log,
207+
);
208+
}
209+
}
210+
mpsc::error::TrySendError::Closed(_) => {
211+
error!(parent: node.span(), "inbound channel closed for {addr}");
212+
break;
213+
}
180214
}
215+
} else if dropped_count != 0 {
216+
warn_about_dropped_messages(&node, addr, &mut dropped_count, &mut last_drop_log);
217+
debug!(parent: node.span(), "the inbound queue for {addr} is no longer saturated");
181218
}
182219
#[cfg(feature = "metrics")]
183220
metrics::increment_gauge(metrics::tcp::TCP_TASKS, 1f64);
184221
}
185-
Err(e) => {
222+
Some(Err(e)) => {
186223
error!(parent: node.span(), "can't read from {addr}: {e}");
187224
node.known_peers().register_failure(addr.ip());
188225
if node.config().fatal_io_errors.contains(&e.kind()) {
189226
break;
190227
}
191228
}
229+
None => break, // end of stream
192230
}
193231
}
194232

@@ -245,3 +283,15 @@ impl<D: Decoder> Decoder for CountingCodec<D> {
245283
Ok(ret)
246284
}
247285
}
286+
287+
/// Warns that some messages were dropped and resets the related counters.
288+
fn warn_about_dropped_messages(node: &Tcp, addr: SocketAddr, dropped_count: &mut usize, last_drop_log: &mut Instant) {
289+
warn!(
290+
parent: node.span(),
291+
"dropped {dropped_count} messages from {addr} due\
292+
to inbound queue saturation",
293+
);
294+
// reset counters
295+
*dropped_count = 0;
296+
*last_drop_log = Instant::now();
297+
}

0 commit comments

Comments
 (0)