Skip to content

Commit 92f639b

Browse files
v2.1: add fanout to tpu-client-next (backport of #3478) (#3523)
* add fanout to tpu-client-next (#3478) * Add tpu-client-next to the root Cargo.toml * Change LeaderUpdater trait to accept mut self * add fanout to the tpu-client-next * Shutdown in separate task * Use try_send instead, minor impromenets * fix LeaderUpdaterError traits * improve lifetimes in split_leaders Co-authored-by: Illia Bobyr <[email protected]> * address PR comments * create connections in advance * removed lookahead_slots --------- Co-authored-by: Illia Bobyr <[email protected]> (cherry picked from commit 2a618b5) # Conflicts: # Cargo.toml * resolve the conflict --------- Co-authored-by: kirill lykov <[email protected]>
1 parent 970606e commit 92f639b

File tree

7 files changed

+336
-224
lines changed

7 files changed

+336
-224
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,7 @@ solana-test-validator = { path = "test-validator", version = "=2.1.3" }
513513
solana-thin-client = { path = "thin-client", version = "=2.1.3" }
514514
solana-transaction-error = { path = "sdk/transaction-error", version = "=2.1.3" }
515515
solana-tpu-client = { path = "tpu-client", version = "=2.1.3", default-features = false }
516+
solana-tpu-client-next = { path = "tpu-client-next", version = "=2.1.3" }
516517
solana-transaction-status = { path = "transaction-status", version = "=2.1.3" }
517518
solana-transaction-status-client-types = { path = "transaction-status-client-types", version = "=2.1.3" }
518519
solana-transaction-metrics-tracker = { path = "transaction-metrics-tracker", version = "=2.1.3" }

tpu-client-next/src/connection_worker.rs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ use {
1414
clock::{DEFAULT_MS_PER_SLOT, MAX_PROCESSING_AGE, NUM_CONSECUTIVE_LEADER_SLOTS},
1515
timing::timestamp,
1616
},
17-
std::net::SocketAddr,
17+
std::{
18+
net::SocketAddr,
19+
sync::{atomic::Ordering, Arc},
20+
},
1821
tokio::{
1922
sync::mpsc,
2023
time::{sleep, Duration},
@@ -72,7 +75,7 @@ pub(crate) struct ConnectionWorker {
7275
connection: ConnectionState,
7376
skip_check_transaction_age: bool,
7477
max_reconnect_attempts: usize,
75-
send_txs_stats: SendTransactionStats,
78+
send_txs_stats: Arc<SendTransactionStats>,
7679
cancel: CancellationToken,
7780
}
7881

@@ -93,6 +96,7 @@ impl ConnectionWorker {
9396
transactions_receiver: mpsc::Receiver<TransactionBatch>,
9497
skip_check_transaction_age: bool,
9598
max_reconnect_attempts: usize,
99+
send_txs_stats: Arc<SendTransactionStats>,
96100
) -> (Self, CancellationToken) {
97101
let cancel = CancellationToken::new();
98102

@@ -103,7 +107,7 @@ impl ConnectionWorker {
103107
connection: ConnectionState::NotSetup,
104108
skip_check_transaction_age,
105109
max_reconnect_attempts,
106-
send_txs_stats: SendTransactionStats::default(),
110+
send_txs_stats,
107111
cancel: cancel.clone(),
108112
};
109113

@@ -155,11 +159,6 @@ impl ConnectionWorker {
155159
}
156160
}
157161

158-
/// Retrieves the statistics for transactions sent by this worker.
159-
pub fn transaction_stats(&self) -> &SendTransactionStats {
160-
&self.send_txs_stats
161-
}
162-
163162
/// Sends a batch of transactions using the provided `connection`.
164163
///
165164
/// Each transaction in the batch is sent over the QUIC streams one at the
@@ -183,11 +182,12 @@ impl ConnectionWorker {
183182

184183
if let Err(error) = result {
185184
trace!("Failed to send transaction over stream with error: {error}.");
186-
record_error(error, &mut self.send_txs_stats);
185+
record_error(error, &self.send_txs_stats);
187186
self.connection = ConnectionState::Retry(0);
188187
} else {
189-
self.send_txs_stats.successfully_sent =
190-
self.send_txs_stats.successfully_sent.saturating_add(1);
188+
self.send_txs_stats
189+
.successfully_sent
190+
.fetch_add(1, Ordering::Relaxed);
191191
}
192192
}
193193
measure_send.stop();
@@ -221,14 +221,14 @@ impl ConnectionWorker {
221221
}
222222
Err(err) => {
223223
warn!("Connection error {}: {}", self.peer, err);
224-
record_error(err.into(), &mut self.send_txs_stats);
224+
record_error(err.into(), &self.send_txs_stats);
225225
self.connection =
226226
ConnectionState::Retry(max_retries_attempt.saturating_add(1));
227227
}
228228
}
229229
}
230230
Err(connecting_error) => {
231-
record_error(connecting_error.clone().into(), &mut self.send_txs_stats);
231+
record_error(connecting_error.clone().into(), &self.send_txs_stats);
232232
match connecting_error {
233233
ConnectError::EndpointStopping => {
234234
debug!("Endpoint stopping, exit connection worker.");

tpu-client-next/src/connection_workers_scheduler.rs

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ use {
99
create_client_config, create_client_endpoint, QuicClientCertificate, QuicError,
1010
},
1111
transaction_batch::TransactionBatch,
12-
workers_cache::{WorkerInfo, WorkersCache, WorkersCacheError},
12+
workers_cache::{maybe_shutdown_worker, WorkerInfo, WorkersCache, WorkersCacheError},
13+
SendTransactionStats,
1314
},
1415
log::*,
1516
quinn::Endpoint,
@@ -39,6 +40,25 @@ pub enum ConnectionWorkersSchedulerError {
3940
LeaderReceiverDropped,
4041
}
4142

43+
/// [`Fanout`] is a configuration struct that specifies how many leaders should
44+
/// be targeted when sending transactions and connecting.
45+
///
46+
/// Note, that the unit is number of leaders per
47+
/// [`NUM_CONSECUTIVE_LEADER_SLOTS`]. It means that if the leader schedule is
48+
/// [L1, L1, L1, L1, L1, L1, L1, L1, L2, L2, L2, L2], the leaders per
49+
/// consecutive leader slots are [L1, L1, L2], so there are 3 of them.
50+
///
51+
/// The idea of having a separate `connect` parameter is to create a set of
52+
/// nodes to connect to in advance in order to hide the latency of opening new
53+
/// connection. Hence, `connect` must be greater or equal to `send`
54+
pub struct Fanout {
55+
/// The number of leaders to target for sending transactions.
56+
pub send: usize,
57+
58+
/// The number of leaders to target for establishing connections.
59+
pub connect: usize,
60+
}
61+
4262
/// Configuration for the [`ConnectionWorkersScheduler`].
4363
///
4464
/// This struct holds the necessary settings to initialize and manage connection
@@ -66,10 +86,8 @@ pub struct ConnectionWorkersSchedulerConfig {
6686
/// connection failure.
6787
pub max_reconnect_attempts: usize,
6888

69-
/// The number of slots to look ahead during the leader estimation
70-
/// procedure. Determines how far into the future leaders are estimated,
71-
/// allowing connections to be established with those leaders in advance.
72-
pub lookahead_slots: u64,
89+
/// Configures the number of leaders to connect to and send transactions to.
90+
pub leaders_fanout: Fanout,
7391
}
7492

7593
impl ConnectionWorkersScheduler {
@@ -90,7 +108,7 @@ impl ConnectionWorkersScheduler {
90108
skip_check_transaction_age,
91109
worker_channel_size,
92110
max_reconnect_attempts,
93-
lookahead_slots,
111+
leaders_fanout,
94112
}: ConnectionWorkersSchedulerConfig,
95113
mut leader_updater: Box<dyn LeaderUpdater>,
96114
mut transaction_receiver: mpsc::Receiver<TransactionBatch>,
@@ -99,6 +117,7 @@ impl ConnectionWorkersScheduler {
99117
let endpoint = Self::setup_endpoint(bind, validator_identity)?;
100118
debug!("Client endpoint bind address: {:?}", endpoint.local_addr());
101119
let mut workers = WorkersCache::new(num_connections, cancel.clone());
120+
let mut send_stats_per_addr = SendTransactionStatsPerAddr::new();
102121

103122
loop {
104123
let transaction_batch = tokio::select! {
@@ -114,50 +133,49 @@ impl ConnectionWorkersScheduler {
114133
break;
115134
}
116135
};
117-
let updated_leaders = leader_updater.next_leaders(lookahead_slots);
118-
let new_leader = &updated_leaders[0];
119-
let future_leaders = &updated_leaders[1..];
120-
if !workers.contains(new_leader) {
121-
debug!("No existing workers for {new_leader:?}, starting a new one.");
122-
let worker = Self::spawn_worker(
123-
&endpoint,
124-
new_leader,
125-
worker_channel_size,
126-
skip_check_transaction_age,
127-
max_reconnect_attempts,
128-
);
129-
workers.push(*new_leader, worker).await;
130-
}
131136

132-
tokio::select! {
133-
send_res = workers.send_transactions_to_address(new_leader, transaction_batch) => match send_res {
134-
Ok(()) => (),
135-
Err(WorkersCacheError::ShutdownError) => {
136-
debug!("Connection to {new_leader} was closed, worker cache shutdown");
137-
}
138-
Err(err) => {
139-
warn!("Connection to {new_leader} was closed, worker error: {err}");
140-
// If we has failed to send batch, it will be dropped.
141-
}
142-
},
143-
() = cancel.cancelled() => {
144-
debug!("Cancelled: Shutting down");
145-
break;
146-
}
147-
};
137+
let updated_leaders = leader_updater.next_leaders(leaders_fanout.connect);
148138

149-
// Regardless of who is leader, add future leaders to the cache to
150-
// hide the latency of opening the connection.
151-
for peer in future_leaders {
139+
let (fanout_leaders, connect_leaders) =
140+
split_leaders(&updated_leaders, &leaders_fanout);
141+
// add future leaders to the cache to hide the latency of opening
142+
// the connection.
143+
for peer in connect_leaders {
152144
if !workers.contains(peer) {
145+
let stats = send_stats_per_addr.entry(peer.ip()).or_default();
153146
let worker = Self::spawn_worker(
154147
&endpoint,
155148
peer,
156149
worker_channel_size,
157150
skip_check_transaction_age,
158151
max_reconnect_attempts,
152+
stats.clone(),
159153
);
160-
workers.push(*peer, worker).await;
154+
maybe_shutdown_worker(workers.push(*peer, worker));
155+
}
156+
}
157+
158+
for new_leader in fanout_leaders {
159+
if !workers.contains(new_leader) {
160+
warn!("No existing worker for {new_leader:?}, skip sending to this leader.");
161+
continue;
162+
}
163+
164+
let send_res =
165+
workers.try_send_transactions_to_address(new_leader, transaction_batch.clone());
166+
match send_res {
167+
Ok(()) => (),
168+
Err(WorkersCacheError::ShutdownError) => {
169+
debug!("Connection to {new_leader} was closed, worker cache shutdown");
170+
}
171+
Err(WorkersCacheError::ReceiverDropped) => {
172+
// Remove the worker from the cache, if the peer has disconnected.
173+
maybe_shutdown_worker(workers.pop(*new_leader));
174+
}
175+
Err(err) => {
176+
warn!("Connection to {new_leader} was closed, worker error: {err}");
177+
// If we has failed to send batch, it will be dropped.
178+
}
161179
}
162180
}
163181
}
@@ -166,7 +184,7 @@ impl ConnectionWorkersScheduler {
166184

167185
endpoint.close(0u32.into(), b"Closing connection");
168186
leader_updater.stop().await;
169-
Ok(workers.transaction_stats().clone())
187+
Ok(send_stats_per_addr)
170188
}
171189

172190
/// Sets up the QUIC endpoint for the scheduler to handle connections.
@@ -191,6 +209,7 @@ impl ConnectionWorkersScheduler {
191209
worker_channel_size: usize,
192210
skip_check_transaction_age: bool,
193211
max_reconnect_attempts: usize,
212+
stats: Arc<SendTransactionStats>,
194213
) -> WorkerInfo {
195214
let (txs_sender, txs_receiver) = mpsc::channel(worker_channel_size);
196215
let endpoint = endpoint.clone();
@@ -202,12 +221,31 @@ impl ConnectionWorkersScheduler {
202221
txs_receiver,
203222
skip_check_transaction_age,
204223
max_reconnect_attempts,
224+
stats,
205225
);
206226
let handle = tokio::spawn(async move {
207227
worker.run().await;
208-
worker.transaction_stats().clone()
209228
});
210229

211230
WorkerInfo::new(txs_sender, handle, cancel)
212231
}
213232
}
233+
234+
/// Splits `leaders` into two slices based on the `fanout` configuration:
235+
/// * the first slice contains the leaders to which transactions will be sent,
236+
/// * the second vector contains the leaders, used to warm up connections. This
237+
/// slice includes the the first set.
238+
fn split_leaders<'leaders>(
239+
leaders: &'leaders [SocketAddr],
240+
fanout: &Fanout,
241+
) -> (&'leaders [SocketAddr], &'leaders [SocketAddr]) {
242+
let Fanout { send, connect } = fanout;
243+
assert!(send <= connect);
244+
let send_count = (*send).min(leaders.len());
245+
let connect_count = (*connect).min(leaders.len());
246+
247+
let send_slice = &leaders[..send_count];
248+
let connect_slice = &leaders[..connect_count];
249+
250+
(send_slice, connect_slice)
251+
}

tpu-client-next/src/leader_updater.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use {
1313
log::*,
1414
solana_connection_cache::connection_cache::Protocol,
1515
solana_rpc_client::nonblocking::rpc_client::RpcClient,
16+
solana_sdk::clock::NUM_CONSECUTIVE_LEADER_SLOTS,
1617
solana_tpu_client::nonblocking::tpu_client::LeaderTpuService,
1718
std::{
1819
fmt,
@@ -22,26 +23,30 @@ use {
2223
Arc,
2324
},
2425
},
26+
thiserror::Error,
2527
};
2628

2729
/// [`LeaderUpdater`] trait abstracts out functionality required for the
2830
/// [`ConnectionWorkersScheduler`](crate::ConnectionWorkersScheduler) to
2931
/// identify next leaders to send transactions to.
3032
#[async_trait]
3133
pub trait LeaderUpdater: Send {
32-
/// Returns next unique leaders for the next `lookahead_slots` starting from
34+
/// Returns next leaders for the next `lookahead_leaders` starting from
3335
/// current estimated slot.
3436
///
37+
/// Leaders are returned per [`NUM_CONSECUTIVE_LEADER_SLOTS`] to avoid unnecessary repetition.
38+
///
3539
/// If the current leader estimation is incorrect and transactions are sent to
3640
/// only one estimated leader, there is a risk of losing all the transactions,
3741
/// depending on the forwarding policy.
38-
fn next_leaders(&self, lookahead_slots: u64) -> Vec<SocketAddr>;
42+
fn next_leaders(&mut self, lookahead_leaders: usize) -> Vec<SocketAddr>;
3943

4044
/// Stop [`LeaderUpdater`] and releases all associated resources.
4145
async fn stop(&mut self);
4246
}
4347

4448
/// Error type for [`LeaderUpdater`].
49+
#[derive(Error, PartialEq)]
4550
pub struct LeaderUpdaterError;
4651

4752
impl fmt::Display for LeaderUpdaterError {
@@ -98,7 +103,9 @@ struct LeaderUpdaterService {
98103

99104
#[async_trait]
100105
impl LeaderUpdater for LeaderUpdaterService {
101-
fn next_leaders(&self, lookahead_slots: u64) -> Vec<SocketAddr> {
106+
fn next_leaders(&mut self, lookahead_leaders: usize) -> Vec<SocketAddr> {
107+
let lookahead_slots =
108+
(lookahead_leaders as u64).saturating_mul(NUM_CONSECUTIVE_LEADER_SLOTS);
102109
self.leader_tpu_service.leader_tpu_sockets(lookahead_slots)
103110
}
104111

@@ -116,7 +123,7 @@ struct PinnedLeaderUpdater {
116123

117124
#[async_trait]
118125
impl LeaderUpdater for PinnedLeaderUpdater {
119-
fn next_leaders(&self, _lookahead_slots: u64) -> Vec<SocketAddr> {
126+
fn next_leaders(&mut self, _lookahead_leaders: usize) -> Vec<SocketAddr> {
120127
self.address.clone()
121128
}
122129

0 commit comments

Comments
 (0)