sui_core/validator_client_monitor/
monitor.rs1use crate::authority_aggregator::AuthorityAggregator;
5use crate::authority_client::AuthorityAPI;
6use crate::validator_client_monitor::stats::ClientObservedStats;
7use crate::validator_client_monitor::{
8 OperationFeedback, OperationType, TransactionClass, metrics::ValidatorClientMetrics,
9};
10use arc_swap::ArcSwap;
11use parking_lot::RwLock;
12use rand::seq::SliceRandom;
13use std::collections::HashMap;
14use std::{
15 sync::{
16 Arc,
17 atomic::{AtomicBool, Ordering},
18 },
19 time::{Duration, Instant},
20};
21use sui_config::validator_client_monitor_config::ValidatorClientMonitorConfig;
22use sui_types::committee::{Committee, CommitteeTrait as _};
23use sui_types::{
24 base_types::{AuthorityName, ConciseableName as _},
25 messages_grpc::ValidatorHealthRequest,
26};
27use tokio::{
28 sync::Notify,
29 task::JoinSet,
30 time::{interval, timeout},
31};
32use tracing::{debug, info, warn};
33
34const REQUESTED_HEALTH_CHECK_SPACING: Duration = Duration::from_secs(2);
38const REQUESTED_HEALTH_CHECKS_PER_INTERVAL: u32 = 2;
45
46pub struct ValidatorClientMonitor<A: Clone> {
58 config: ValidatorClientMonitorConfig,
59 metrics: Arc<ValidatorClientMetrics>,
60 client_stats: RwLock<ClientObservedStats>,
61 authority_aggregator: Arc<ArcSwap<AuthorityAggregator<A>>>,
62 cached_latencies: RwLock<HashMap<(AuthorityName, TransactionClass), Duration>>,
63 staggering_reports: RwLock<HashMap<AuthorityName, (bool, Instant)>>,
66 staggering_active: AtomicBool,
69 health_check_now: Notify,
72 requested_round_in_progress: AtomicBool,
75}
76
77impl<A> ValidatorClientMonitor<A>
78where
79 A: AuthorityAPI + Send + Sync + 'static + Clone,
80{
81 pub fn new(
82 config: ValidatorClientMonitorConfig,
83 metrics: Arc<ValidatorClientMetrics>,
84 authority_aggregator: Arc<ArcSwap<AuthorityAggregator<A>>>,
85 ) -> Arc<Self> {
86 info!(
87 "Validator client monitor starting with config: {:?}",
88 config
89 );
90
91 let monitor = Arc::new(Self {
92 config: config.clone(),
93 metrics,
94 client_stats: RwLock::new(ClientObservedStats::new(config)),
95 authority_aggregator,
96 cached_latencies: RwLock::new(HashMap::new()),
97 staggering_reports: RwLock::new(HashMap::new()),
98 staggering_active: AtomicBool::new(false),
99 health_check_now: Notify::new(),
100 requested_round_in_progress: AtomicBool::new(false),
101 });
102
103 let monitor_clone = monitor.clone();
104 tokio::spawn(async move {
105 monitor_clone.run_health_checks().await;
106 });
107
108 monitor
109 }
110
111 #[cfg(test)]
112 pub fn new_for_test(authority_aggregator: Arc<AuthorityAggregator<A>>) -> Arc<Self> {
113 use prometheus::Registry;
114
115 Self::new(
116 ValidatorClientMonitorConfig::default(),
117 Arc::new(ValidatorClientMetrics::new(&Registry::default())),
118 Arc::new(ArcSwap::new(authority_aggregator)),
119 )
120 }
121
122 async fn run_health_checks(self: Arc<Self>) {
128 let mut interval = interval(self.config.health_check_interval);
129 let mut last_round_started = tokio::time::Instant::now() - REQUESTED_HEALTH_CHECK_SPACING;
130 let mut requested_rounds_left = REQUESTED_HEALTH_CHECKS_PER_INTERVAL;
131
132 loop {
133 let requested = tokio::select! {
134 _ = interval.tick() => {
135 requested_rounds_left = REQUESTED_HEALTH_CHECKS_PER_INTERVAL;
136 false
137 }
138 _ = self.health_check_now.notified() => {
139 if self.staggering_active() || requested_rounds_left == 0 {
142 continue;
143 }
144 requested_rounds_left -= 1;
145 tokio::time::sleep_until(last_round_started + REQUESTED_HEALTH_CHECK_SPACING)
146 .await;
147 interval.reset();
149 true
150 }
151 };
152 last_round_started = tokio::time::Instant::now();
153 self.requested_round_in_progress
154 .store(requested, Ordering::Relaxed);
155
156 let authority_agg = self.authority_aggregator.load();
157
158 let current_validators: Vec<_> = authority_agg.committee.names().cloned().collect();
159 self.client_stats
160 .write()
161 .retain_validators(¤t_validators);
162 self.staggering_reports
163 .write()
164 .retain(|name, _| current_validators.contains(name));
165
166 let mut tasks = JoinSet::new();
167
168 for (name, safe_client) in authority_agg.authority_clients.iter() {
169 let name = *name;
170 let display_name = authority_agg.get_display_name(&name);
171 let client = safe_client.clone();
172 let timeout_duration = self.config.health_check_timeout;
173 let monitor = self.clone();
174
175 tasks.spawn(async move {
176 let start = Instant::now();
177 match timeout(
178 timeout_duration,
179 client.validator_health(ValidatorHealthRequest {}),
180 )
181 .await
182 {
183 Ok(Ok(response)) => {
184 monitor.record_staggering_report(name, response.staggering_active);
185 let latency = start.elapsed();
186 monitor.record_interaction_result(OperationFeedback {
187 authority_name: name,
188 display_name: display_name.clone(),
189 operation: OperationType::HealthCheck,
190 tx_class: None,
191 ping_type: None,
192 result: Ok(latency),
193 });
194 }
195 Ok(Err(_)) => {
196 let _latency = start.elapsed();
197 monitor.record_interaction_result(OperationFeedback {
198 authority_name: name,
199 display_name: display_name.clone(),
200 operation: OperationType::HealthCheck,
201 tx_class: None,
202 ping_type: None,
203 result: Err(()),
204 });
205 }
206 Err(_) => {
207 monitor.record_interaction_result(OperationFeedback {
208 authority_name: name,
209 display_name,
210 operation: OperationType::HealthCheck,
211 tx_class: None,
212 ping_type: None,
213 result: Err(()),
214 });
215 }
216 }
217 });
218 }
219
220 while let Some(result) = tasks.join_next().await {
221 if let Err(e) = result {
222 warn!("Health check task failed: {}", e);
223 }
224 }
225
226 self.update_cached_latencies(&authority_agg);
227 self.refresh_staggering_view("end of round");
229 self.requested_round_in_progress
230 .store(false, Ordering::Relaxed);
231 }
232 }
233}
234
235impl<A: Clone> ValidatorClientMonitor<A> {
236 fn update_cached_latencies(&self, authority_agg: &AuthorityAggregator<A>) {
242 let committee = &authority_agg.committee;
243 let mut cached_latencies = self.cached_latencies.write();
244
245 for tx_class in [TransactionClass::Restricted, TransactionClass::Unrestricted] {
246 let latencies_map = self
247 .client_stats
248 .read()
249 .get_all_validator_stats(committee, tx_class);
250
251 for (validator, latency) in latencies_map.iter() {
252 debug!(
253 "Validator {}, class {:?}, latency {}",
254 validator,
255 tx_class,
256 latency.as_secs_f64()
257 );
258 let display_name = authority_agg.get_display_name(validator);
259 self.metrics
260 .performance
261 .with_label_values(&[display_name.as_str(), tx_class.as_str()])
262 .set(latency.as_secs_f64());
263 cached_latencies.insert((*validator, tx_class), *latency);
264 }
265 }
266 }
267
268 pub fn record_interaction_result(&self, feedback: OperationFeedback) {
276 let operation_str = match feedback.operation {
277 OperationType::Submit => "submit",
278 OperationType::Effects => "effects",
279 OperationType::HealthCheck => "health_check",
280 OperationType::SingleWriterFinality => "single_writer_finality",
281 OperationType::SharedObjectFinality => "shared_object_finality",
282 };
283 let ping_label = if feedback.ping_type.is_some() {
284 "true"
285 } else {
286 "false"
287 };
288
289 match feedback.result {
290 Ok(latency) => {
291 self.metrics
292 .observed_latency
293 .with_label_values(&[feedback.display_name.as_str(), operation_str, ping_label])
294 .observe(latency.as_secs_f64());
295 self.metrics
296 .operation_success
297 .with_label_values(&[feedback.display_name.as_str(), operation_str, ping_label])
298 .inc();
299 }
300 Err(()) => {
301 self.metrics
302 .operation_failure
303 .with_label_values(&[feedback.display_name.as_str(), operation_str, ping_label])
304 .inc();
305 }
306 }
307
308 let mut client_stats = self.client_stats.write();
309 client_stats.record_interaction_result(feedback);
310 }
311
312 pub fn has_observed_latencies(&self) -> bool {
316 !self.cached_latencies.read().is_empty()
317 }
318
319 fn report_freshness(&self) -> Duration {
323 self.config.health_check_interval * 2
324 }
325
326 pub fn record_staggering_report(&self, validator: AuthorityName, active: Option<bool>) {
331 let Some(active) = active else {
332 return;
333 };
334 self.staggering_reports
335 .write()
336 .insert(validator, (active, Instant::now()));
337 self.refresh_staggering_view(&format!("report from {}", validator.concise()));
338 }
339
340 pub fn request_health_check(&self) {
348 self.health_check_now.notify_one();
349 }
350
351 pub fn staggering_active(&self) -> bool {
360 self.staggering_active.load(Ordering::Relaxed)
361 }
362
363 fn refresh_staggering_view(&self, trigger: &str) {
366 let authority_agg = self.authority_aggregator.load();
367 let committee = &authority_agg.committee;
368 let freshness = self.report_freshness();
369 let now = Instant::now();
370 let active_stake: u64 = self
371 .staggering_reports
372 .read()
373 .iter()
374 .filter(|(_, (active, received_at))| {
375 *active && now.saturating_duration_since(*received_at) < freshness
376 })
377 .map(|(name, _)| committee.weight(name))
378 .sum();
379 let active = active_stake >= committee.validity_threshold();
380 if self.staggering_active.swap(active, Ordering::Relaxed) != active {
381 info!(
382 "Staggering view flipped {}: {} of {} stake reports staggering active \
383 (validity threshold {}), on {} during a {} health-check round",
384 if active { "on" } else { "off" },
385 active_stake,
386 committee.total_votes(),
387 committee.validity_threshold(),
388 trigger,
389 if self.requested_round_in_progress.load(Ordering::Relaxed) {
390 "requested"
391 } else {
392 "scheduled"
393 },
394 );
395 }
396 self.metrics
397 .staggering_active_stake
398 .set(active_stake as i64);
399 self.metrics.staggering_active.set(active as i64);
400 }
401
402 #[cfg(test)]
403 pub fn record_staggering_report_at(&self, validator: AuthorityName, active: bool, at: Instant) {
404 self.staggering_reports
405 .write()
406 .insert(validator, (active, at));
407 self.refresh_staggering_view("test report");
408 }
409
410 pub fn select_shuffled_preferred_validators(
424 &self,
425 committee: &Committee,
426 delta: f64,
427 tx_class: TransactionClass,
428 ) -> Vec<AuthorityName> {
429 let mut rng = rand::thread_rng();
430
431 let cached_latencies = self.cached_latencies.read();
432 if cached_latencies.is_empty() {
433 let mut validators: Vec<_> = committee.names().cloned().collect();
434 validators.shuffle(&mut rng);
435 return validators;
436 };
437
438 let mut validator_with_latencies: Vec<_> = committee
441 .names()
442 .map(|v| {
443 (
444 *v,
445 cached_latencies
446 .get(&(*v, tx_class))
447 .cloned()
448 .unwrap_or(Duration::ZERO),
449 )
450 })
451 .collect();
452 if validator_with_latencies.is_empty() {
453 return vec![];
454 }
455 validator_with_latencies.shuffle(&mut rng);
457 validator_with_latencies.sort_by_key(|(_, latency)| *latency);
459
460 let lowest_latency = validator_with_latencies[0].1;
462 let threshold = lowest_latency.mul_f64(1.0 + delta);
463 let k = validator_with_latencies
464 .iter()
465 .enumerate()
466 .find(|(_, (_, latency))| *latency > threshold)
467 .map(|(i, _)| i)
468 .unwrap_or(validator_with_latencies.len());
469 validator_with_latencies[..k].shuffle(&mut rng);
470 self.metrics.shuffled_validators.observe(k as f64);
471
472 validator_with_latencies
473 .into_iter()
474 .map(|(v, _)| v)
475 .collect()
476 }
477
478 #[cfg(test)]
479 pub fn force_update_cached_latencies(&self, authority_agg: &AuthorityAggregator<A>) {
480 self.update_cached_latencies(authority_agg);
481 }
482
483 #[cfg(test)]
484 pub fn get_client_stats_for_test(
485 &self,
486 validator: &AuthorityName,
487 ) -> crate::validator_client_monitor::stats::ValidatorClientStats {
488 self.client_stats
489 .read()
490 .validator_stats
491 .get(validator)
492 .cloned()
493 .unwrap()
494 }
495
496 #[cfg(test)]
497 pub fn get_client_stats_len(&self) -> usize {
498 self.client_stats.read().validator_stats.len()
499 }
500
501 #[cfg(test)]
502 pub fn has_validator_stats(&self, validator: &AuthorityName) -> bool {
503 self.client_stats
504 .read()
505 .validator_stats
506 .contains_key(validator)
507 }
508}