1use std::{sync::Arc, time::Duration};
20
21use consensus_config::Stake;
22use consensus_types::block::Round;
23use futures::stream::{FuturesUnordered, StreamExt as _};
24use mysten_common::sync::notify_once::NotifyOnce;
25use mysten_metrics::monitored_scope;
26use parking_lot::RwLock;
27use tokio::{task::JoinHandle, time::MissedTickBehavior};
28
29use crate::{
30 BlockAPI as _, ConsumerMonitor, context::Context, core_thread::CoreThreadDispatcher,
31 dag_state::DagState, network::ValidatorNetworkClient, round_tracker::RoundTracker,
32 task::join_and_propagate_panic,
33};
34
35pub(crate) struct RoundProberHandle {
37 prober_task: JoinHandle<()>,
38 shutdown_notify: Arc<NotifyOnce>,
39}
40
41impl RoundProberHandle {
42 pub(crate) async fn stop(self) {
43 let _ = self.shutdown_notify.notify();
44 join_and_propagate_panic(self.prober_task).await;
46 }
47}
48
49pub(crate) struct RoundProber<C: ValidatorNetworkClient> {
50 context: Arc<Context>,
51 core_thread_dispatcher: Arc<dyn CoreThreadDispatcher>,
52 round_tracker: Arc<RwLock<RoundTracker>>,
53 dag_state: Arc<RwLock<DagState>>,
54 network_client: Arc<C>,
55 consumer_monitor: Arc<ConsumerMonitor>,
56 shutdown_notify: Arc<NotifyOnce>,
57}
58
59impl<C: ValidatorNetworkClient> RoundProber<C> {
60 pub(crate) fn new(
61 context: Arc<Context>,
62 core_thread_dispatcher: Arc<dyn CoreThreadDispatcher>,
63 round_tracker: Arc<RwLock<RoundTracker>>,
64 dag_state: Arc<RwLock<DagState>>,
65 network_client: Arc<C>,
66 consumer_monitor: Arc<ConsumerMonitor>,
67 ) -> Self {
68 Self {
69 context,
70 core_thread_dispatcher,
71 round_tracker,
72 dag_state,
73 network_client,
74 consumer_monitor,
75 shutdown_notify: Arc::new(NotifyOnce::new()),
76 }
77 }
78
79 pub(crate) fn start(self) -> RoundProberHandle {
80 let shutdown_notify = self.shutdown_notify.clone();
81 let loop_shutdown_notify = shutdown_notify.clone();
82 let prober_task = tokio::spawn(async move {
83 let mut interval = tokio::time::interval(Duration::from_millis(
87 self.context.parameters.round_prober_interval_ms,
88 ));
89 interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
90 loop {
91 tokio::select! {
92 _ = interval.tick() => {
93 self.probe().await;
94 }
95 _ = loop_shutdown_notify.wait() => {
96 break;
97 }
98 }
99 }
100 });
101 RoundProberHandle {
102 prober_task,
103 shutdown_notify,
104 }
105 }
106
107 pub(crate) async fn probe(&self) -> Round {
110 let _scope = monitored_scope("RoundProber");
111
112 let node_metrics = &self.context.metrics.node_metrics;
113 let request_timeout =
114 Duration::from_millis(self.context.parameters.round_prober_request_timeout_ms);
115 let own_index = self.context.own_index;
116 let mut requests = FuturesUnordered::new();
117
118 for (peer, _) in self.context.committee.authorities() {
119 if peer == own_index {
120 continue;
121 }
122 let network_client = self.network_client.clone();
123 requests.push(async move {
124 let result = tokio::time::timeout(
125 request_timeout,
126 network_client.get_latest_rounds(peer, request_timeout),
127 )
128 .await;
129 (peer, result)
130 });
131 }
132
133 let mut highest_received_rounds =
134 vec![vec![0; self.context.committee.size()]; self.context.committee.size()];
135 let mut highest_accepted_rounds =
136 vec![vec![0; self.context.committee.size()]; self.context.committee.size()];
137
138 let blocks = self
139 .dag_state
140 .read()
141 .get_last_cached_block_per_authority(Round::MAX);
142 let local_highest_accepted_rounds = blocks
143 .into_iter()
144 .map(|(block, _)| block.round())
145 .collect::<Vec<_>>();
146 let last_proposed_round = local_highest_accepted_rounds[own_index];
147
148 highest_received_rounds[own_index] =
151 self.round_tracker.read().local_highest_received_rounds();
152 highest_accepted_rounds[own_index] = local_highest_accepted_rounds;
153 highest_received_rounds[own_index][own_index] = last_proposed_round;
154 highest_accepted_rounds[own_index][own_index] = last_proposed_round;
155
156 let mut responsive_stake: Stake = 0;
159
160 loop {
161 tokio::select! {
162 result = requests.next() => {
163 let Some((peer, result)) = result else { break };
164 let peer_name = &self.context.committee.authority(peer).hostname;
165 match result {
166 Ok(Ok((received, accepted))) => {
167 responsive_stake += self.context.committee.stake(peer);
168 if received.len() == self.context.committee.size()
169 {
170 highest_received_rounds[peer] = received;
171 } else {
172 node_metrics.round_prober_request_errors.with_label_values(&["invalid_received_rounds"]).inc();
173 tracing::warn!("Received invalid number of received rounds from peer {}", peer_name);
174 }
175
176 if accepted.len() == self.context.committee.size() {
177 highest_accepted_rounds[peer] = accepted;
178 } else {
179 node_metrics.round_prober_request_errors.with_label_values(&["invalid_accepted_rounds"]).inc();
180 tracing::warn!("Received invalid number of accepted rounds from peer {}", peer_name);
181 }
182 },
183 Ok(Err(err)) => {
194 node_metrics.round_prober_request_errors.with_label_values(&["failed_fetch"]).inc();
195 tracing::debug!("Failed to get latest rounds from peer {}: {:?}", peer_name, err);
196 },
197 Err(_) => {
198 node_metrics.round_prober_request_errors.with_label_values(&["timeout"]).inc();
199 tracing::debug!("Timeout while getting latest rounds from peer {}", peer_name);
200 },
201 }
202 }
203 _ = self.shutdown_notify.wait() => break,
204 }
205 }
206
207 self.round_tracker
208 .write()
209 .update_from_probe(highest_accepted_rounds, highest_received_rounds);
210 let propagation_delay = self
211 .round_tracker
212 .read()
213 .calculate_propagation_delay(last_proposed_round);
214
215 let _ = self
216 .core_thread_dispatcher
217 .set_propagation_delay(propagation_delay);
218
219 if requests.is_empty() {
223 let quorum_accepted_round = self.round_tracker.read().highest_accepted_quorum_round();
224 self.consumer_monitor.report_probe(
225 quorum_accepted_round,
226 responsive_stake,
227 self.context.clock.timestamp_utc_ms(),
228 );
229 }
230
231 propagation_delay
232 }
233}
234
235#[cfg(test)]
236mod test {
237 use std::{
238 collections::BTreeSet,
239 sync::{
240 Arc,
241 atomic::{AtomicBool, Ordering},
242 },
243 time::Duration,
244 };
245
246 use async_trait::async_trait;
247 use bytes::Bytes;
248 use consensus_config::AuthorityIndex;
249 use consensus_types::block::{BlockRef, Round};
250 use parking_lot::RwLock;
251
252 use crate::{
253 ConsumerMonitor, TestBlock, VerifiedBlock,
254 commit::{CertifiedCommits, CommitRange},
255 context::Context,
256 core_thread::{CoreError, CoreThreadDispatcher},
257 dag_state::DagState,
258 error::{ConsensusError, ConsensusResult},
259 network::{BlockStream, ValidatorNetworkClient},
260 round_prober::RoundProber,
261 round_tracker::RoundTracker,
262 storage::mem_store::MemStore,
263 };
264
265 struct FakeThreadDispatcher {}
266
267 impl FakeThreadDispatcher {
268 fn new() -> Self {
269 Self {}
270 }
271 }
272
273 #[async_trait]
274 impl CoreThreadDispatcher for FakeThreadDispatcher {
275 async fn add_blocks(
276 &self,
277 _blocks: Vec<VerifiedBlock>,
278 ) -> Result<BTreeSet<BlockRef>, CoreError> {
279 unimplemented!()
280 }
281
282 async fn check_block_refs(
283 &self,
284 _block_refs: Vec<BlockRef>,
285 ) -> Result<BTreeSet<BlockRef>, CoreError> {
286 unimplemented!()
287 }
288
289 async fn add_certified_commits(
290 &self,
291 _commits: CertifiedCommits,
292 ) -> Result<BTreeSet<BlockRef>, CoreError> {
293 unimplemented!()
294 }
295
296 async fn new_block(&self, _round: Round, _force: bool) -> Result<(), CoreError> {
297 unimplemented!()
298 }
299
300 async fn get_missing_blocks(&self) -> Result<BTreeSet<BlockRef>, CoreError> {
301 unimplemented!()
302 }
303
304 fn set_propagation_delay(&self, _propagation_delay: Round) -> Result<(), CoreError> {
305 Ok(())
306 }
307
308 fn set_last_known_proposed_round(&self, _round: Round) -> Result<(), CoreError> {
309 unimplemented!()
310 }
311 }
312
313 struct FakeNetworkClient {
314 highest_received_rounds: RwLock<Vec<Vec<Round>>>,
315 highest_accepted_rounds: RwLock<Vec<Vec<Round>>>,
316 hanging: AtomicBool,
318 }
319
320 impl FakeNetworkClient {
321 fn new(
322 highest_received_rounds: Vec<Vec<Round>>,
323 highest_accepted_rounds: Vec<Vec<Round>>,
324 ) -> Self {
325 Self {
326 highest_received_rounds: RwLock::new(highest_received_rounds),
327 highest_accepted_rounds: RwLock::new(highest_accepted_rounds),
328 hanging: AtomicBool::new(false),
329 }
330 }
331
332 fn set_rounds(
334 &self,
335 highest_received_rounds: Vec<Vec<Round>>,
336 highest_accepted_rounds: Vec<Vec<Round>>,
337 ) {
338 *self.highest_received_rounds.write() = highest_received_rounds;
339 *self.highest_accepted_rounds.write() = highest_accepted_rounds;
340 }
341 }
342
343 #[async_trait]
344 impl ValidatorNetworkClient for FakeNetworkClient {
345 #[cfg(not(msim))]
346 async fn send_block(
347 &self,
348 _peer: AuthorityIndex,
349 _serialized_block: &VerifiedBlock,
350 _timeout: Duration,
351 ) -> ConsensusResult<()> {
352 unimplemented!("Unimplemented")
353 }
354
355 async fn subscribe_blocks(
356 &self,
357 _peer: AuthorityIndex,
358 _last_received: Round,
359 _timeout: Duration,
360 ) -> ConsensusResult<BlockStream> {
361 unimplemented!("Unimplemented")
362 }
363
364 async fn fetch_blocks(
365 &self,
366 _peer: AuthorityIndex,
367 _block_refs: Vec<BlockRef>,
368 _fetch_after_rounds: Vec<Round>,
369 _fetch_missing_ancestors: bool,
370 _timeout: Duration,
371 ) -> ConsensusResult<Vec<Bytes>> {
372 unimplemented!("Unimplemented")
373 }
374
375 async fn fetch_commits(
376 &self,
377 _peer: AuthorityIndex,
378 _commit_range: CommitRange,
379 _timeout: Duration,
380 ) -> ConsensusResult<(Vec<Bytes>, Vec<Bytes>)> {
381 unimplemented!("Unimplemented")
382 }
383
384 async fn fetch_latest_blocks(
385 &self,
386 _peer: AuthorityIndex,
387 _authorities: Vec<AuthorityIndex>,
388 _timeout: Duration,
389 ) -> ConsensusResult<Vec<Bytes>> {
390 unimplemented!("Unimplemented")
391 }
392
393 async fn get_latest_rounds(
394 &self,
395 peer: AuthorityIndex,
396 _timeout: Duration,
397 ) -> ConsensusResult<(Vec<Round>, Vec<Round>)> {
398 if self.hanging.load(Ordering::Relaxed) {
399 return std::future::pending().await;
400 }
401 let received_rounds = self.highest_received_rounds.read()[peer].clone();
402 let accepted_rounds = self.highest_accepted_rounds.read()[peer].clone();
403 if received_rounds.is_empty() && accepted_rounds.is_empty() {
404 Err(ConsensusError::NetworkRequestTimeout("test".to_string()))
405 } else {
406 Ok((received_rounds, accepted_rounds))
407 }
408 }
409 }
410
411 #[tokio::test]
412 async fn test_round_prober() {
413 telemetry_subscribers::init_for_testing();
414 const NUM_AUTHORITIES: usize = 7;
415 let context = Arc::new(Context::new_for_test(NUM_AUTHORITIES).0);
416 let core_thread_dispatcher = Arc::new(FakeThreadDispatcher::new());
417 let store = Arc::new(MemStore::new());
418 let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
419 let network_client = Arc::new(FakeNetworkClient::new(
421 vec![
422 vec![],
423 vec![109, 121, 131, 0, 151, 161, 171],
424 vec![101, 0, 103, 104, 105, 166, 107],
425 vec![],
426 vec![100, 102, 133, 0, 155, 106, 177],
427 vec![105, 115, 103, 0, 125, 126, 127],
428 vec![10, 20, 30, 40, 50, 60],
429 ], vec![
431 vec![],
432 vec![0, 121, 131, 0, 151, 161, 171],
433 vec![1, 0, 103, 104, 105, 166, 107],
434 vec![],
435 vec![0, 102, 133, 0, 155, 106, 177],
436 vec![1, 115, 103, 0, 125, 126, 127],
437 vec![1, 20, 30, 40, 50, 60],
438 ], ));
440
441 let round_tracker = Arc::new(RwLock::new(RoundTracker::new(
443 context.clone(),
444 vec![110, 120, 130, 140, 150, 160, 170],
445 )));
446 let consumer_monitor = Arc::new(ConsumerMonitor::new(0, Some(0)));
447 let prober = RoundProber::new(
448 context.clone(),
449 core_thread_dispatcher.clone(),
450 round_tracker.clone(),
451 dag_state.clone(),
452 network_client.clone(),
453 consumer_monitor.clone(),
454 );
455
456 let blocks = (0..NUM_AUTHORITIES)
458 .map(|authority| {
459 let round = 110 + (authority as u32 * 10);
460 VerifiedBlock::new_for_test(TestBlock::new(round, authority as u32).build())
461 })
462 .collect::<Vec<_>>();
463
464 dag_state.write().accept_blocks(blocks);
465
466 let propagation_delay = prober.probe().await;
477
478 assert_eq!(propagation_delay, 10);
479
480 let peer_progress = consumer_monitor.peer_progress();
485 assert_eq!(peer_progress.responsive_stake, 5);
486 assert_eq!(peer_progress.quorum_accepted_round, 107);
487 assert_eq!(peer_progress.quorum_commit_index, 0);
488 assert!(peer_progress.probed_at_ms > 0);
489 }
490
491 #[tokio::test(flavor = "current_thread", start_paused = true)]
492 async fn round_prober_publishes_peer_progress() {
493 telemetry_subscribers::init_for_testing();
494 const NUM_AUTHORITIES: usize = 4;
495 let context = Arc::new(Context::new_for_test(NUM_AUTHORITIES).0);
496 let core_thread_dispatcher = Arc::new(FakeThreadDispatcher::new());
497 let store = Arc::new(MemStore::new());
498 let dag_state = Arc::new(RwLock::new(DagState::new(context.clone(), store)));
499 let round_tracker = Arc::new(RwLock::new(RoundTracker::new(context.clone(), vec![])));
500 let consumer_monitor = Arc::new(ConsumerMonitor::new(0, Some(0)));
501
502 let blocks = (0..NUM_AUTHORITIES)
504 .map(|authority| {
505 VerifiedBlock::new_for_test(TestBlock::new(20, authority as u32).build())
506 })
507 .collect::<Vec<_>>();
508 dag_state.write().accept_blocks(blocks);
509
510 let answers = |round: Round| {
512 vec![
513 vec![],
514 vec![round; NUM_AUTHORITIES],
515 vec![round; NUM_AUTHORITIES],
516 vec![round; NUM_AUTHORITIES],
517 ]
518 };
519 let network_client = Arc::new(FakeNetworkClient::new(answers(12), answers(12)));
520 let prober = RoundProber::new(
521 context.clone(),
522 core_thread_dispatcher,
523 round_tracker,
524 dag_state,
525 network_client.clone(),
526 consumer_monitor.clone(),
527 );
528
529 prober.probe().await;
532 let first = consumer_monitor.peer_progress();
533 assert_eq!(first.quorum_accepted_round, 12);
534 assert_eq!(first.responsive_stake, 3);
535 assert_eq!(first.quorum_commit_index, 0);
536 assert!(first.probed_at_ms > 0);
537
538 network_client.set_rounds(vec![vec![]; NUM_AUTHORITIES], vec![vec![]; NUM_AUTHORITIES]);
541 tokio::time::advance(Duration::from_millis(1_000)).await;
542 prober.probe().await;
543 let second = consumer_monitor.peer_progress();
544 assert_eq!(second.quorum_accepted_round, 0);
545 assert_eq!(second.responsive_stake, 0);
546 assert!(second.probed_at_ms >= first.probed_at_ms + 1_000);
547
548 let mut partial = answers(30);
551 partial[3] = vec![];
552 network_client.set_rounds(partial.clone(), partial);
553 tokio::time::advance(Duration::from_millis(1_000)).await;
554 prober.probe().await;
555 let third = consumer_monitor.peer_progress();
556 assert_eq!(third.quorum_accepted_round, 20);
557 assert_eq!(third.responsive_stake, 2);
558 assert!(third.probed_at_ms >= second.probed_at_ms + 1_000);
559
560 network_client.hanging.store(true, Ordering::Relaxed);
563 tokio::time::advance(Duration::from_millis(1_000)).await;
564 let probe = prober.probe();
565 tokio::pin!(probe);
566 assert!(futures::poll!(&mut probe).is_pending());
567 prober.shutdown_notify.notify().unwrap();
568 probe.await;
569 assert_eq!(consumer_monitor.peer_progress(), third);
570 }
571}