From 455d845c04a19e49f4b778a44af8d4963b622f03 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Thu, 31 Jul 2025 15:40:25 -0400 Subject: [PATCH 01/20] util: ema + rate + stream + testkit --- yellowstone-grpc-geyser/src/lib.rs | 1 + yellowstone-grpc-geyser/src/util/ema.rs | 321 ++++++++++++++++ yellowstone-grpc-geyser/src/util/mod.rs | 5 + yellowstone-grpc-geyser/src/util/rate.rs | 382 ++++++++++++++++++++ yellowstone-grpc-geyser/src/util/shed.rs | 8 + yellowstone-grpc-geyser/src/util/stream.rs | 344 ++++++++++++++++++ yellowstone-grpc-geyser/src/util/testkit.rs | 23 ++ 7 files changed, 1084 insertions(+) create mode 100644 yellowstone-grpc-geyser/src/util/ema.rs create mode 100644 yellowstone-grpc-geyser/src/util/mod.rs create mode 100644 yellowstone-grpc-geyser/src/util/rate.rs create mode 100644 yellowstone-grpc-geyser/src/util/shed.rs create mode 100644 yellowstone-grpc-geyser/src/util/stream.rs create mode 100644 yellowstone-grpc-geyser/src/util/testkit.rs diff --git a/yellowstone-grpc-geyser/src/lib.rs b/yellowstone-grpc-geyser/src/lib.rs index 41aa4025..23049237 100644 --- a/yellowstone-grpc-geyser/src/lib.rs +++ b/yellowstone-grpc-geyser/src/lib.rs @@ -2,6 +2,7 @@ pub mod config; pub mod grpc; pub mod metrics; pub mod plugin; +pub(crate) mod util; pub mod version; pub fn get_thread_name() -> String { diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs new file mode 100644 index 00000000..63697fa0 --- /dev/null +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -0,0 +1,321 @@ +use std::{ + fmt::{self, Display}, + str::FromStr, + sync::{ + atomic::{AtomicU64, Ordering}, + RwLock, + }, + time::{Duration, Instant}, +}; + +/// +/// Exponential Moving Average (EMA) for load tracking. +/// +#[derive(Debug)] +pub struct Ema { + window: Duration, + window_load: AtomicU64, + current_load_ema: AtomicU64, + period: u64, + last_update: RwLock, +} + +/// +/// Exponential Moving Average (EMA) reactivity categories. +/// +/// These categories define how much weight the EMA gives to recent values compared to older ones. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum EMAReactivity { + /// Very reactive, attribute 50% weight to the most recent value. + VeryReactive, + /// Reactive, attribute 33% weight to the most recent value. + #[default] + Reactive, + /// Moderately reactive, attribute 20% weight to the most recent value. + ModeratelyReactive, + /// Less reactive, attribute 10% weight to the most recent value. + LessReactive, +} + +impl fmt::Display for EMAReactivity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + EMAReactivity::VeryReactive => write!(f, "Very Reactive"), + EMAReactivity::Reactive => write!(f, "Reactive"), + EMAReactivity::ModeratelyReactive => write!(f, "Moderately Reactive"), + EMAReactivity::LessReactive => write!(f, "Less Reactive"), + } + } +} + +impl FromStr for EMAReactivity { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "very reactive" => Ok(EMAReactivity::VeryReactive), + "reactive" => Ok(EMAReactivity::Reactive), + "moderately reactive" => Ok(EMAReactivity::ModeratelyReactive), + "less reactive" => Ok(EMAReactivity::LessReactive), + _ => Err("Invalid EMA reactivity"), + } + } +} + +impl EMAReactivity { + const fn as_period(self) -> u64 { + match self { + EMAReactivity::VeryReactive => 3, + EMAReactivity::Reactive => 5, + EMAReactivity::ModeratelyReactive => 10, + EMAReactivity::LessReactive => 20, + } + } +} + +#[derive(Debug, Clone)] +pub struct EMACurrentLoad { + ema_load: u64, + unit: Duration, +} + +impl Display for EMACurrentLoad { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} / {}ms", self.ema_load, self.unit.as_millis()) + } +} + +impl EMACurrentLoad { + /// + /// Converts the current traffic load native unit to taffic per seconds. + /// + pub fn per_second(&self) -> f64 { + let scale = Duration::from_secs(1).as_millis() as f64 / self.unit.as_millis() as f64; + self.ema_load as f64 * scale + } +} + +pub const DEFAULT_EMA_WINDOW: Duration = Duration::from_millis(10); + +impl Ema { + pub fn new(window: Duration, reactivity: EMAReactivity) -> Self { + Self::with_starting_time(window, reactivity, Instant::now()) + } + + pub const fn with_starting_time( + window: Duration, + reactivity: EMAReactivity, + starting_time: Instant, + ) -> Self { + Self { + window, + window_load: AtomicU64::new(0), + period: reactivity.as_period(), + last_update: RwLock::new(starting_time), + current_load_ema: AtomicU64::new(0), + } + } + + #[inline] + pub fn alpha(&self) -> f64 { + 2.0 / (self.period as f64 + 1.0) + } + + fn ema_function(&self, current_ema: u64, recent_load: u32) -> u64 { + let alpha = self.alpha(); + let beta = 1.0 - alpha; + (alpha * recent_load as f64 + beta * current_ema as f64) as _ + } + + fn update_ema(&self, duration_since_last_update: Duration) { + // + let time_since_last_update = duration_since_last_update.as_millis(); + + // If the time since the last update is bigger than the window, we need to catch up and update the EMA + // for each missed update. + let extra_updates = time_since_last_update.saturating_sub(1) / self.window.as_millis(); + let load_in_recent_window = self.window_load.swap(0, Ordering::Relaxed) as u32; // Cast to u32 + + let mut updated_load_ema = self.ema_function( + self.current_load_ema.load(Ordering::Relaxed), + load_in_recent_window, + ); + + if extra_updates > 0 { + log::trace!( + "Updating EMA with {} extra updates, current EMA: {}, load in recent window: {}", + extra_updates, + updated_load_ema, + load_in_recent_window + ); + } + for _ in 0..extra_updates { + updated_load_ema = self.ema_function(updated_load_ema, load_in_recent_window); + } + + self.current_load_ema + .store(updated_load_ema, Ordering::Relaxed); + } + + #[inline] + pub fn current_load_ema_in_native_unit(&self) -> u64 { + self.current_load_ema.load(Ordering::Relaxed) + } + + pub fn current_load(&self) -> EMACurrentLoad { + let ema_load = self.current_load_ema_in_native_unit(); + let unit = self.window; + EMACurrentLoad { ema_load, unit } + } + + fn update_ema_if_needed(&self, now: Instant) { + let last_update = *self.last_update.read().unwrap(); + if now.duration_since(last_update) >= self.window { + let mut last_update = self.last_update.write().unwrap(); + // check again if the last update is still older than the window since it might have been updated by another thread in the meantime + let since_last_update = now.duration_since(*last_update); + if since_last_update >= self.window { + log::trace!("Updating EMA at {:?}", now); + *last_update = now; + self.update_ema(since_last_update); + } else { + log::trace!( + "Skipping EMA update at {:?}, concurrent update is too recent", + now + ); + } + } + } + + pub fn record_load(&self, now: Instant, load: u32) { + self.window_load.fetch_add(load as u64, Ordering::Relaxed); + self.update_ema_if_needed(now); + } +} + +#[cfg(test)] +mod tests { + use std::{fmt::Write as _, str::FromStr}; + + #[test] + fn test_emareactivity_from_str() { + assert_eq!( + EMAReactivity::from_str("Very Reactive").unwrap(), + EMAReactivity::VeryReactive + ); + assert_eq!( + EMAReactivity::from_str("Reactive").unwrap(), + EMAReactivity::Reactive + ); + assert_eq!( + EMAReactivity::from_str("Moderately Reactive").unwrap(), + EMAReactivity::ModeratelyReactive + ); + assert_eq!( + EMAReactivity::from_str("Less Reactive").unwrap(), + EMAReactivity::LessReactive + ); + assert!(EMAReactivity::from_str("invalid").is_err()); + } + + #[test] + fn test_emareactivity_display() { + let mut s = String::new(); + write!(&mut s, "{}", EMAReactivity::VeryReactive).unwrap(); + assert_eq!(s, "Very Reactive"); + s.clear(); + write!(&mut s, "{}", EMAReactivity::Reactive).unwrap(); + assert_eq!(s, "Reactive"); + s.clear(); + write!(&mut s, "{}", EMAReactivity::ModeratelyReactive).unwrap(); + assert_eq!(s, "Moderately Reactive"); + s.clear(); + write!(&mut s, "{}", EMAReactivity::LessReactive).unwrap(); + assert_eq!(s, "Less Reactive"); + } + + #[test] + fn test_emareactivity_as_period() { + assert_eq!(EMAReactivity::VeryReactive.as_period(), 3); + assert_eq!(EMAReactivity::Reactive.as_period(), 5); + assert_eq!(EMAReactivity::ModeratelyReactive.as_period(), 10); + assert_eq!(EMAReactivity::LessReactive.as_period(), 20); + } + + #[test] + fn test_ema_alpha_for_all_reactivities() { + let window = Duration::from_secs(10); + let very = Ema::new(window, EMAReactivity::VeryReactive); + let reactive = Ema::new(window, EMAReactivity::Reactive); + let moderate = Ema::new(window, EMAReactivity::ModeratelyReactive); + let less = Ema::new(window, EMAReactivity::LessReactive); + assert!((very.alpha() - 0.5).abs() < 1e-6); + assert!((reactive.alpha() - 0.3333333).abs() < 1e-6); + assert!((moderate.alpha() - 0.1818181).abs() < 1e-6); + assert!((less.alpha() - 0.0952380).abs() < 1e-6); + } + + #[test] + fn test_ema_function_computation() { + let window = Duration::from_secs(10); + let ema = Ema::new(window, EMAReactivity::Reactive); // period = 5, alpha = 0.333... + // current_ema = 6, recent_load = 12 + // expected = alpha * recent_load + (1-alpha) * current_ema + let expected = (0.33333333333 * 12.0 + 0.6666667 * 6.0) as u64; + assert_eq!(ema.ema_function(6, 12), expected); + } + use { + super::*, + std::time::{Duration, Instant}, + }; + #[test] + fn test_ema_new_initializes_fields() { + let window = Duration::from_secs(10); + let ema = Ema::new(window, EMAReactivity::Reactive); + assert_eq!(ema.window, window); + assert_eq!(ema.period, EMAReactivity::Reactive.as_period()); + assert_eq!(ema.current_load_ema_in_native_unit(), 0); + } + + #[test] + fn test_record_load_increments_and_updates_ema_after_window() { + let now = Instant::now(); + const WINDOW: Duration = Duration::from_millis(1); + let ema = Ema::with_starting_time(WINDOW, EMAReactivity::VeryReactive, now); + let now_1w = now + WINDOW; + ema.record_load(now, 1); + ema.record_load(now, 1); + ema.record_load(now_1w, 1); + assert_eq!(ema.current_load_ema_in_native_unit(), 1); + } + + #[test] + fn recent_load_change_should_not_change_last_ema() { + let now = Instant::now(); + const WINDOW: Duration = Duration::from_millis(1); + let ema = Ema::with_starting_time(WINDOW, EMAReactivity::VeryReactive, now); + // Add 3 load in the current EMA window. + ema.record_load(now, 1); + ema.record_load(now, 1); + let now_1w = now + WINDOW; + ema.record_load(now_1w, 1); + println!("alpha: {}", ema.alpha()); + let expected_ema = ema.ema_function(0, 3); + println!("Expected EMA: {}", expected_ema); + assert_eq!(ema.current_load_ema_in_native_unit(), expected_ema); + + // adding more to the new window should not change the EMA + ema.record_load(now_1w, 1); + ema.record_load(now_1w, 1); + ema.record_load(now_1w, 1); + + assert_eq!(ema.current_load_ema_in_native_unit(), expected_ema); + } + + #[test] + fn test_alpha_calculation() { + let ema = Ema::new(Duration::from_secs(10), EMAReactivity::VeryReactive); + let alpha = ema.alpha(); + assert!(alpha > 0.0 && alpha < 1.0); + } +} diff --git a/yellowstone-grpc-geyser/src/util/mod.rs b/yellowstone-grpc-geyser/src/util/mod.rs new file mode 100644 index 00000000..b0dc6c6c --- /dev/null +++ b/yellowstone-grpc-geyser/src/util/mod.rs @@ -0,0 +1,5 @@ +pub mod ema; +pub mod rate; +pub mod stream; +#[cfg(test)] +pub(crate) mod testkit; diff --git a/yellowstone-grpc-geyser/src/util/rate.rs b/yellowstone-grpc-geyser/src/util/rate.rs new file mode 100644 index 00000000..e90458b7 --- /dev/null +++ b/yellowstone-grpc-geyser/src/util/rate.rs @@ -0,0 +1,382 @@ +use std::{ + fmt, + sync::atomic::{AtomicU64, Ordering}, + time::{Duration, Instant}, +}; + +struct Bin { + timestamp: AtomicU64, // seconds since start_time + sum: AtomicU64, + sum_of_squares: AtomicU64, // Sum of squares for variance calculation +} + +/// +/// Tracks the rate of traffic per second over a specified time window. +/// +pub struct RateTracker { + bins: Box<[Bin]>, + start_time: Instant, + window: Duration, // e.g., 1 hour +} + +impl fmt::Debug for RateTracker { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RateTracker") + .field("bins", &self.bins.len()) + .field("start_time", &self.start_time) + .field("window", &self.window) + .finish() + } +} + +impl RateTracker { + pub fn new(window: Duration) -> Self { + let now = Instant::now(); + Self::with_starting_time(window, now) + } + + pub fn with_starting_time(window: Duration, now: Instant) -> Self { + let bins = (0..window.as_secs()) + .map(|_| Bin { + timestamp: AtomicU64::new(0), + sum: AtomicU64::new(0), + sum_of_squares: AtomicU64::new(0), + }) + .collect::>() + .into_boxed_slice(); + + Self { + bins, + start_time: now, + window, + } + } + + pub fn record(&self, now: Instant, traffic: u32) { + if now < self.start_time { + return; // Ignore records before the start time + } + let now_secs = now.duration_since(self.start_time).as_secs(); + let bin_index = (now_secs % self.bins.len() as u64) as usize; + let bin = &self.bins[bin_index]; + let ts = bin.timestamp.load(Ordering::Acquire); + if ts != now_secs { + // Try to update timestamp to current second. + // Use AcqRel on success to synchronize with count reset, + // Acquire on failure to see fresh state. + if bin + .timestamp + .compare_exchange(ts, now_secs, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + // This thread won the race; clear the count. + // Use Release ordering to ensure the store happens after timestamp update. + bin.sum.store(0, Ordering::Release); + bin.sum_of_squares.store(0, Ordering::Release); + } + // If CAS failed, another thread updated timestamp & count; do nothing. + } + // Increment count atomically. Relaxed ordering is fine here. + bin.sum.fetch_add(traffic as u64, Ordering::Relaxed); + bin.sum_of_squares + .fetch_add((traffic as u64) * (traffic as u64), Ordering::Relaxed); + } + + pub fn average_rate(&self, now: Instant) -> f64 { + if now < self.start_time { + return 0.0; // No events recorded yet + } + let now_secs = now.duration_since(self.start_time).as_secs(); + let min_ts = now_secs.saturating_sub(self.window.as_secs()); + let max_bin = self.bins.len().min(now_secs as usize); + let mut sum = 0.0; + for bin in self.bins.iter().take(max_bin) { + let ts = bin.timestamp.load(Ordering::Relaxed); + if ts < min_ts { + continue; // Skip bins outside the window + } + let bin_sum = bin.sum.load(Ordering::Relaxed); + sum += bin_sum as f64; + } + if (max_bin as f64) == 0.0 { + return 0.0; // No bins to average + } + sum / max_bin as f64 + } + + pub fn variance(&self, now: Instant) -> f64 { + if now < self.start_time { + return 0.0; // No events recorded yet + } + let now_secs = now.duration_since(self.start_time).as_secs(); + let min_ts = now_secs.saturating_sub(self.window.as_secs()); + let max_bin = self.bins.len().min(now_secs as usize); + + if max_bin == 0 { + return 0.0; // No bins to calculate variance + } + + let mut sum = 0u64; + let mut sum_of_squares = 0u64; + + for bin in self.bins.iter().take(max_bin) { + let bin_sum = bin.sum.load(Ordering::Relaxed); + let sq = bin.sum_of_squares.load(Ordering::Relaxed); + let ts = bin.timestamp.load(Ordering::Relaxed); + if ts < min_ts { + continue; // Skip bins outside the window + } + sum += bin_sum; + sum_of_squares += sq; + } + + let mean = sum as f64 / max_bin as f64; + let mean_of_squares = sum_of_squares as f64 / max_bin as f64; + mean_of_squares - (mean * mean) + } + + pub fn stddev(&self, now: Instant) -> f64 { + self.variance(now).sqrt() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + std::time::{Duration, Instant}, + }; + + #[test] + fn test_rate_tracker_initialization() { + let window = Duration::from_secs(10); + let tracker = RateTracker::new(window); + assert_eq!(tracker.bins.len(), 10); + assert_eq!(tracker.window, window); + } + + #[test] + fn test_no_recorded_traffic() { + let now = Instant::now(); + let window = Duration::from_secs(10); + let tracker = RateTracker::with_starting_time(window, now); + + let rate = tracker.average_rate(now + Duration::from_secs(10)); + let variance = tracker.variance(now + Duration::from_secs(10)); + let stddev = tracker.stddev(now + Duration::from_secs(10)); + + assert_eq!(rate, 0.0); + assert_eq!(variance, 0.0); + assert_eq!(stddev, 0.0); + } + + #[test] + fn test_record_increments_count() { + let now = Instant::now(); + let window = Duration::from_secs(5); + let tracker = RateTracker::with_starting_time(window, now); + + tracker.record(now, 1); + tracker.record(now, 1); + + let now_secs = Instant::now().duration_since(tracker.start_time).as_secs(); + let bin_index = (now_secs % tracker.bins.len() as u64) as usize; + let bin = &tracker.bins[bin_index]; + + assert_eq!(bin.sum.load(Ordering::Relaxed), 2); + } + + #[test] + fn test_average_rate_calculation() { + let now = Instant::now(); + let window = Duration::from_secs(5); + let tracker = RateTracker::with_starting_time(window, now); + + // Simulate recording events + let now_1s = now + Duration::from_secs(1); + let now_2s = now + Duration::from_secs(2); + tracker.record(now, 1); + tracker.record(now, 1); + tracker.record(now_1s, 1); + + // (2 + 1) / 2 = 1.5 + let rate = tracker.average_rate(now_2s); + assert_eq!(rate, 1.5); + } + + #[test] + fn test_variance_and_stddev_calculation() { + let now = Instant::now(); + let window = Duration::from_secs(5); + let tracker = RateTracker::with_starting_time(window, now); + + // Simulate recording events + let now_1s = now + Duration::from_secs(1); + let now_2s = now + Duration::from_secs(2); + let now_3s = now + Duration::from_secs(3); + tracker.record(now, 5); + tracker.record(now_1s, 10); + tracker.record(now_2s, 15); + + let variance = tracker.variance(now_3s); + let stddev = tracker.stddev(now_3s); + + assert!(variance >= 16.6 && variance <= 16.7); // Variance should be around 16.67 + assert!((stddev * stddev - variance).abs() < 1e-10); + } + + #[test] + fn test_multiple_bins_update() { + let now = Instant::now(); + let window = Duration::from_secs(10); + let tracker = RateTracker::with_starting_time(window, now); + + // Simulate events across multiple bins + for i in 0..10 { + let event_time = now + Duration::from_secs(i); + tracker.record(event_time, (i as u32) + 1); // Cast to u32 + } + + // Ensure all bins are within the window by advancing the time + let end_time = now + Duration::from_secs(10); + + let rate = tracker.average_rate(end_time); + let variance = tracker.variance(end_time); + let stddev = tracker.stddev(end_time); + + assert_eq!(rate, 5.5); // Average of 1 through 10 + assert!(variance > 0.0); + assert!(stddev > 0.0); + } + + #[test] + fn test_no_events_recorded() { + let now = Instant::now(); + let window = Duration::from_secs(10); + let tracker = RateTracker::with_starting_time(window, now); + + let rate = tracker.average_rate(now + Duration::from_secs(10)); + let variance = tracker.variance(now + Duration::from_secs(10)); + let stddev = tracker.stddev(now + Duration::from_secs(10)); + + assert_eq!(rate, 0.0); + assert_eq!(variance, 0.0); + assert_eq!(stddev, 0.0); + } + + #[test] + fn test_all_events_outside_window() { + let now = Instant::now(); + let window = Duration::from_secs(10); + let tracker = RateTracker::with_starting_time(window, now); + + // Record events outside the window + tracker.record(now - Duration::from_secs(20), 5 as u32); // Cast to u32 + tracker.record(now - Duration::from_secs(15), 10 as u32); // Cast to u32 + + let rate = tracker.average_rate(now); + let variance = tracker.variance(now); + let stddev = tracker.stddev(now); + + assert_eq!(rate, 0.0); + assert_eq!(variance, 0.0); + assert_eq!(stddev, 0.0); + } + + #[test] + fn test_no_variance() { + let now = Instant::now(); + let window = Duration::from_secs(10); + let tracker = RateTracker::with_starting_time(window, now); + let now_1s = now + Duration::from_secs(1); + let now_2s = now + Duration::from_secs(2); + // Simulate a high volume of events in a single bin + tracker.record(now, 1_000_000 as u32); // Cast to u32 + tracker.record(now_1s, 1_000_000 as u32); // Cast to u32 + + let rate = tracker.average_rate(now_2s); + let variance = tracker.variance(now_2s); + let stddev = tracker.stddev(now_2s); + + assert_eq!(rate, 1_000_000.0); + assert!(variance == 0.0); + assert!(stddev == 0.0); + } + + #[test] + fn test_rate_cyclicity() { + let now = Instant::now(); + let window = Duration::from_secs(3600); // 1 hour + let tracker = RateTracker::with_starting_time(window, now); + let end_first_period = now + Duration::from_secs(3600); + let end_second_period = now + Duration::from_secs(7200); + for i in 0..3600 { + let event_time = now + Duration::from_secs(i); + tracker.record(event_time, i as u32 + 1); + } + // Sum of 1 to 3600 = 6_481_800 + let rate = tracker.average_rate(end_first_period); + assert_eq!(rate, 6_481_800.0 / 3600.0); + + for i in 3600..7200 { + let event_time = now + Duration::from_secs(i); + tracker.record(event_time, i as u32); + } + // Sum of 3660 to 7199 = 19_438_200 + let rate = tracker.average_rate(end_second_period); + assert_eq!(rate, 19_438_200.0 / 3600.0); + + let variance = tracker.variance(end_second_period); + let stddev = tracker.stddev(end_second_period); + assert!(variance > 0.0); + assert!((stddev * stddev - variance).abs() < 1e-5); + } + + #[test] + fn test_long_period_of_inactivity() { + let now = Instant::now(); + let window = Duration::from_secs(3600); // 1 hour + let tracker = RateTracker::with_starting_time(window, now); + let end_first_period = now + Duration::from_secs(3600); + for i in 0..3600 { + let event_time = now + Duration::from_secs(i); + tracker.record(event_time, i as u32 + 1); + } + + let rate = tracker.average_rate(end_first_period); + assert_eq!(rate, 6_481_800.0 / 3600.0); + + // Simulate a long period of inactivity + let long_inactivity = now + Duration::from_secs(7200); + let rate = tracker.average_rate(long_inactivity); + let var = tracker.variance(long_inactivity); + let stddev = tracker.stddev(long_inactivity); + assert_eq!(rate, 0.0); // No events recorded in the second hour + assert_eq!(var, 0.0); // Variance should also be zero + assert_eq!(stddev, 0.0); // Standard deviation should also be zero + assert!((stddev * stddev - var).abs() < 1e-10); + } + + #[test] + fn it_should_handle_holes_in_history() { + let now = Instant::now(); + let window = Duration::from_secs(10); + let tracker = RateTracker::with_starting_time(window, now); + let one_sec = Duration::from_secs(1); + // Record events at 1s and 3s, skipping 2s + tracker.record(now, 1); // 0th second + tracker.record(now + one_sec, 2); // 1st second + // 2nd second is skipped + tracker.record(now + (one_sec * 3), 4); // 3rd second + + // Average rate should consider only the recorded events + let expected_avg = (1 + 2 + 0 + 4) as f64 / 4.0; + let rate = tracker.average_rate(now + Duration::from_secs(4)); + assert_eq!(rate, expected_avg); + + let variance = tracker.variance(now + Duration::from_secs(4)); + let stddev = tracker.stddev(now + Duration::from_secs(4)); + assert_eq!(variance, 2.1875); + assert!((stddev * stddev - variance).abs() < 1e-10); + } +} diff --git a/yellowstone-grpc-geyser/src/util/shed.rs b/yellowstone-grpc-geyser/src/util/shed.rs new file mode 100644 index 00000000..b2ef9435 --- /dev/null +++ b/yellowstone-grpc-geyser/src/util/shed.rs @@ -0,0 +1,8 @@ +use std::time::Instant; + +#[derive(Default)] +pub struct ClientShedder { + first_throttle_hint: Option, +} + +impl ClientShedder {} diff --git a/yellowstone-grpc-geyser/src/util/stream.rs b/yellowstone-grpc-geyser/src/util/stream.rs new file mode 100644 index 00000000..9bb4f64c --- /dev/null +++ b/yellowstone-grpc-geyser/src/util/stream.rs @@ -0,0 +1,344 @@ +use { + crate::util::{ + ema::{EMACurrentLoad, EMAReactivity, Ema, DEFAULT_EMA_WINDOW}, + rate::RateTracker, + }, + futures::Stream, + std::{ + sync::{ + atomic::AtomicU64, // Removed AtomicU32 + Arc, + }, + task::{Context, Poll}, + time::{Duration, Instant}, + }, + tokio::sync::mpsc::{ + error::{SendError, TrySendError}, + Receiver, Sender, + }, +}; + +pub trait Weighted { + fn weight(&self) -> u32; +} + +#[derive(Debug)] +struct Shared { + size: AtomicU64, + weight: AtomicU64, // Reverted back to AtomicU64 + send_ema: Ema, + rx_ema: Ema, + avg_weight_rate: RateTracker, +} + +#[derive(Debug, Clone)] +struct ChannelCapacities { + threshold_size: u64, + threshold_weight: u64, +} + +#[derive(Debug, Clone)] +pub struct LoadAwareSender { + shared: Arc, + inner: Sender, +} + +pub struct LoadAwareReceiver { + shared: Arc, + inner: Receiver, +} + +impl Shared { + #[inline] + fn add_load(&self, weight: u32, now: Instant) { + // Kept parameter type as u32 + self.size.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.weight + .fetch_add(weight as u64, std::sync::atomic::Ordering::Relaxed); // Cast weight to u64 + self.avg_weight_rate.record(now, weight); // Cast weight to u64 for compatibility + self.send_ema.record_load(now, weight); // Cast weight to u64 for compatibility + } + + #[inline] + fn decr_load(&self, weight: u32, now: Instant) { + // Kept parameter type as u32 + self.size.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); + self.weight + .fetch_sub(weight as u64, std::sync::atomic::Ordering::Relaxed); // Cast weight to u64 + self.rx_ema.record_load(now, weight); // Cast weight to u64 for compatibility + } +} + +pub const DEFAULT_AVG_TRAFFIC_RATE_WINDOW: Duration = Duration::from_secs(10 * 60); // 10 minutes + +/// +/// Settings for the load-aware channel. +/// This struct defines the parameters for the average traffic rate window and the EMA settings. +/// It allows customization of the channel's behavior regarding load tracking and traffic estimation. +/// +pub struct StatsSettings { + avg_traffic_rate_window: Duration, + tx_ema_window: Duration, + tx_ema_reactivity: EMAReactivity, + rx_ema_window: Duration, + rx_ema_reactivity: EMAReactivity, +} + +impl Default for StatsSettings { + fn default() -> Self { + Self { + avg_traffic_rate_window: DEFAULT_AVG_TRAFFIC_RATE_WINDOW, + tx_ema_window: DEFAULT_EMA_WINDOW, + tx_ema_reactivity: EMAReactivity::Reactive, + rx_ema_window: DEFAULT_EMA_WINDOW, + rx_ema_reactivity: EMAReactivity::LessReactive, // Less reactive for receiving end -> closer to an all-time average + } + } +} + +/// +/// Creates a load-aware channel with the specified capacity and average weight rate window. +/// The sender and receiver can be used to send and receive items that implement the `Weighted` +/// trait, which provides a method to get the "traffic" weight of the item. +/// +/// The word "traffic" is used here to indicate the load or weight of the item being sent. +/// +pub fn load_aware_channel( + capacity: usize, + stats_settings: StatsSettings, +) -> (LoadAwareSender, LoadAwareReceiver) +where + T: Weighted, +{ + let (inner_sender, inner_receiver) = tokio::sync::mpsc::channel(capacity); + + let shared = Arc::new(Shared { + size: AtomicU64::new(0), + weight: AtomicU64::new(0), + send_ema: Ema::new( + stats_settings.tx_ema_window, + stats_settings.tx_ema_reactivity, + ), + rx_ema: Ema::new( + stats_settings.rx_ema_window, + stats_settings.rx_ema_reactivity, + ), + avg_weight_rate: RateTracker::new(stats_settings.avg_traffic_rate_window), + }); + let sender = LoadAwareSender { + shared: Arc::clone(&shared), + inner: inner_sender, + }; + + let rx = LoadAwareReceiver { + shared, + inner: inner_receiver, + }; + + (sender, rx) +} + +/// +/// Sender end of the load-aware channel. +/// +/// See [`load_aware_channel`] for more details. +/// +impl LoadAwareSender +where + T: Weighted, +{ + pub fn estimated_send_rate(&self) -> EMACurrentLoad { + self.shared.send_ema.current_load() + } + + pub fn estimated_consuming_rate(&self) -> EMACurrentLoad { + self.shared.rx_ema.current_load() + } + + pub async fn send(&self, item: T) -> Result<(), SendError> { + let entry_weight = item.weight(); + let now = Instant::now(); + self.inner.send(item).await?; + self.shared.add_load(entry_weight, now); + Ok(()) + } + + pub fn try_send(&self, item: T) -> Result<(), TrySendError> { + let entry_weight = item.weight(); + let now = Instant::now(); + self.inner.try_send(item)?; + self.shared.add_load(entry_weight, now); + Ok(()) + } +} + +/// +/// Receiving end of the load-aware channel. +/// +/// See [`load_aware_channel`] for more details. +/// +impl LoadAwareReceiver +where + T: Weighted, +{ + pub async fn recv(&mut self) -> Option { + use std::future::poll_fn; + poll_fn(|cx| self.poll_recv(cx)).await + } + + pub fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll> { + let shared = Arc::clone(&self.shared); + self.inner.poll_recv(cx).map(|maybe| { + if let Some(entry) = &maybe { + let entry_weight = entry.weight(); + shared.decr_load(entry_weight, Instant::now()); + } + maybe + }) + } + + pub fn estimated_rx_rate(&self) -> EMACurrentLoad { + self.shared.rx_ema.current_load() + } +} + +impl Stream for LoadAwareReceiver +where + T: Weighted, +{ + type Item = T; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let this = self.get_mut(); + this.poll_recv(cx) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::util::testkit, + log::LevelFilter, + std::sync::atomic::Ordering, + tokio::{sync::mpsc, task::yield_now}, + tokio_stream::StreamExt, + }; + + #[derive(Debug)] + struct TestItem(u32); + + impl Weighted for TestItem { + fn weight(&self) -> u32 { + self.0 + } + } + + #[tokio::test] + async fn test_basic_send_and_receive() { + let (sender, mut receiver) = load_aware_channel(10, Default::default()); + + sender.send(TestItem(5)).await.unwrap(); + let received = receiver.recv().await.unwrap(); + + assert_eq!(received.0, 5); + } + + #[tokio::test] + async fn test_load_tracking() { + let (sender, mut receiver) = load_aware_channel(10, Default::default()); + + sender.send(TestItem(5)).await.unwrap(); + assert_eq!(sender.shared.size.load(Ordering::Relaxed), 1); + assert_eq!(sender.shared.weight.load(Ordering::Relaxed), 5); + + receiver.recv().await.unwrap(); + assert_eq!(sender.shared.size.load(Ordering::Relaxed), 0); + assert_eq!(sender.shared.weight.load(Ordering::Relaxed), 0); + } + + #[tokio::test] + async fn test_stream_behavior() { + let (sender, receiver) = load_aware_channel(10, Default::default()); + + sender.send(TestItem(1)).await.unwrap(); + sender.send(TestItem(2)).await.unwrap(); + sender.send(TestItem(3)).await.unwrap(); + + drop(sender); + let mut stream = receiver; + + let mut results = vec![]; + while let Some(item) = stream.next().await { + results.push(item.0); + } + + assert_eq!(results, vec![1, 2, 3]); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_high_send_rate() { + log::set_boxed_logger(Box::new(testkit::StdoutLogger)) + .map(|()| log::set_max_level(LevelFilter::Trace)) + .unwrap(); + + let (sender, mut receiver) = load_aware_channel(100000, Default::default()); + let target_rate_per_s = 1000; + let total_duration = Duration::from_secs(3); + let item_weight = 1; + + let start_time = Instant::now(); + let mut _sent_count = 0; // Renamed to suppress unused variable warning + + let rx_task = tokio::spawn(async move { + let mut cnt = 0; + while let Some(_item) = receiver.recv().await { + cnt += 1; + } + cnt + }); + + let mut send_cnt = 0; + while Instant::now().duration_since(start_time) < total_duration { + sender.send(TestItem(item_weight)).await.unwrap(); + send_cnt += 1; + let now = Instant::now(); + // Busy wait to simulate high send rate + while now.elapsed() < Duration::from_micros(900) { + yield_now().await; + } + } + + // Verify the send rate load + let estimated_send_rate = sender.estimated_send_rate(); + + drop(sender); // Close the sender to stop the receiver + let received_count = rx_task.await.unwrap(); + log::trace!( + "Total items sent: {}, received: {}", + send_cnt, + received_count + ); + assert_eq!( + send_cnt, received_count, + "All sent items should be received" + ); + + assert!( + (estimated_send_rate.per_second() - target_rate_per_s as f64).abs() < 50.0, + "Estimated rate: ~{}/s, target: {} +/- 50 / s", + estimated_send_rate.per_second(), + target_rate_per_s + ); + } + + #[tokio::test] + async fn sender_should_send_error_when_recv_drop() { + let (sender, receiver) = load_aware_channel(10, Default::default()); + drop(receiver); + assert!(sender.send(TestItem(1)).await.is_err()); + } +} diff --git a/yellowstone-grpc-geyser/src/util/testkit.rs b/yellowstone-grpc-geyser/src/util/testkit.rs new file mode 100644 index 00000000..db2f7ebf --- /dev/null +++ b/yellowstone-grpc-geyser/src/util/testkit.rs @@ -0,0 +1,23 @@ +use { + core::panic, + log::{Level, Metadata, Record}, +}; + +/// +/// A simple logger that logs messages to the console. +/// +pub struct StdoutLogger; + +impl log::Log for StdoutLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= log::max_level() + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + println!("{} - {}", record.level(), record.args()); + } + } + + fn flush(&self) {} +} From 1a8e437e974c0b876141ee1bbb2be228084c19a2 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Thu, 31 Jul 2025 15:40:53 -0400 Subject: [PATCH 02/20] fix clippy and rustc issue --- yellowstone-grpc-geyser/src/grpc.rs | 2 +- yellowstone-grpc-proto/src/lib.rs | 5 +++-- yellowstone-grpc-proto/src/plugin/filter/filter.rs | 1 + yellowstone-grpc-proto/src/plugin/message.rs | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index 53e6f725..e7c3bc73 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -252,7 +252,7 @@ struct MessageId { } impl MessageId { - fn next(&mut self) -> u64 { + const fn next(&mut self) -> u64 { self.id = self.id.checked_add(1).expect("message id overflow"); self.id } diff --git a/yellowstone-grpc-proto/src/lib.rs b/yellowstone-grpc-proto/src/lib.rs index 472b460a..1c90d88c 100644 --- a/yellowstone-grpc-proto/src/lib.rs +++ b/yellowstone-grpc-proto/src/lib.rs @@ -147,8 +147,9 @@ pub mod convert_to { rewards, loaded_addresses, return_data, - compute_units_consumed, - cost_units } = meta; + compute_units_consumed, + cost_units, + } = meta; let err = create_transaction_error(status); let inner_instructions_none = inner_instructions.is_none(); let inner_instructions = inner_instructions diff --git a/yellowstone-grpc-proto/src/plugin/filter/filter.rs b/yellowstone-grpc-proto/src/plugin/filter/filter.rs index 305f80e2..c90b40d5 100644 --- a/yellowstone-grpc-proto/src/plugin/filter/filter.rs +++ b/yellowstone-grpc-proto/src/plugin/filter/filter.rs @@ -1167,6 +1167,7 @@ mod tests { loaded_addresses: LoadedAddresses::default(), return_data: None, compute_units_consumed: None, + cost_units: None, }); let sig = sanitized_transaction.signature(); let account_keys = sanitized_transaction diff --git a/yellowstone-grpc-proto/src/plugin/message.rs b/yellowstone-grpc-proto/src/plugin/message.rs index 6bc8aed6..c21e9df2 100644 --- a/yellowstone-grpc-proto/src/plugin/message.rs +++ b/yellowstone-grpc-proto/src/plugin/message.rs @@ -544,6 +544,7 @@ pub enum Message { } impl Message { + #[allow(clippy::missing_const_for_fn)] pub fn get_slot(&self) -> u64 { match self { Self::Slot(msg) => msg.slot, From 86adbbbbea7fbaa56b7d1a069a8922c90e268da4 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Thu, 31 Jul 2025 17:15:57 -0400 Subject: [PATCH 03/20] ema: cap extra updates --- yellowstone-grpc-geyser/src/util/ema.rs | 36 +++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs index 63697fa0..a41cb0a5 100644 --- a/yellowstone-grpc-geyser/src/util/ema.rs +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -16,7 +16,7 @@ pub struct Ema { window: Duration, window_load: AtomicU64, current_load_ema: AtomicU64, - period: u64, + reactivity: EMAReactivity, last_update: RwLock, } @@ -71,6 +71,23 @@ impl EMAReactivity { EMAReactivity::LessReactive => 20, } } + + /// + /// Precomputed table of effective memory usage for each reactivity level. + /// + /// This is how many steps in EMA before the data becomes "irrelevant" to current load computation. + /// Which is another way of saying after how many steps the last record will only contribute to 1% of the prediction. + /// + /// Log_{alpha}(0.01) + /// + const fn effective_memory(self) -> u64 { + match self { + EMAReactivity::VeryReactive => 7, + EMAReactivity::Reactive => 5, + EMAReactivity::ModeratelyReactive => 3, + EMAReactivity::LessReactive => 2, + } + } } #[derive(Debug, Clone)] @@ -110,7 +127,7 @@ impl Ema { Self { window, window_load: AtomicU64::new(0), - period: reactivity.as_period(), + reactivity, last_update: RwLock::new(starting_time), current_load_ema: AtomicU64::new(0), } @@ -118,7 +135,7 @@ impl Ema { #[inline] pub fn alpha(&self) -> f64 { - 2.0 / (self.period as f64 + 1.0) + 2.0 / (self.reactivity.as_period() as f64 + 1.0) } fn ema_function(&self, current_ema: u64, recent_load: u32) -> u64 { @@ -133,7 +150,16 @@ impl Ema { // If the time since the last update is bigger than the window, we need to catch up and update the EMA // for each missed update. - let extra_updates = time_since_last_update.saturating_sub(1) / self.window.as_millis(); + let missed_updates = time_since_last_update.saturating_sub(1) / self.window.as_millis(); + + // Limit the number of extra updates to the effective memory of the reactivity level. + // This is to prevent excessive computation in case of long delays. + // This is a trade-off between accuracy and performance. + // EMA terms forms a geometric series, where each term contributes less and less to the final value. + // The contribution value of each decay exponentially decreases. + // after the effective memory steps, the contribution of the last record is less than 1% of the prediction. + let extra_updates = missed_updates.min(self.reactivity.effective_memory() as u128); + let load_in_recent_window = self.window_load.swap(0, Ordering::Relaxed) as u32; // Cast to u32 let mut updated_load_ema = self.ema_function( @@ -273,7 +299,7 @@ mod tests { let window = Duration::from_secs(10); let ema = Ema::new(window, EMAReactivity::Reactive); assert_eq!(ema.window, window); - assert_eq!(ema.period, EMAReactivity::Reactive.as_period()); + assert_eq!(ema.reactivity, EMAReactivity::Reactive); assert_eq!(ema.current_load_ema_in_native_unit(), 0); } From 167b18eb706966a9992652e25280f0e88dfc45e1 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Thu, 31 Jul 2025 20:21:01 -0400 Subject: [PATCH 04/20] metrics: grpc_message_sent_count --- yellowstone-grpc-geyser/src/grpc.rs | 14 ++++++++++++-- yellowstone-grpc-geyser/src/metrics.rs | 15 +++++++++++++++ yellowstone-grpc-geyser/src/util/ema.rs | 6 +++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index e7c3bc73..6229717b 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -845,6 +845,7 @@ impl GrpcService { #[allow(clippy::too_many_arguments)] async fn client_loop( id: usize, + subscriber_id: Option, endpoint: String, stream_tx: mpsc::Sender>, mut client_rx: mpsc::UnboundedReceiver, Filter)>>, @@ -856,7 +857,7 @@ impl GrpcService { ) { let mut filter = Filter::default(); metrics::update_subscriptions(&endpoint, None, Some(&filter)); - + let subscriber_id = subscriber_id.unwrap_or("UNKNOWN".to_owned()); metrics::connections_total_inc(); DebugClientMessage::maybe_send(&debug_client_tx, || DebugClientMessage::UpdateFilter { id, @@ -947,7 +948,9 @@ impl GrpcService { for (_msgid, message) in messages.iter() { for message in filter.get_updates(message, Some(commitment)) { match stream_tx.send(Ok(message)).await { - Ok(()) => {} + Ok(()) => { + metrics::incr_grpc_message_sent_counter(&subscriber_id); + } Err(mpsc::error::SendError(_)) => { error!("client #{id}: stream closed"); break 'outer; @@ -1142,6 +1145,12 @@ impl Geyser for GrpcService { .and_then(|h| h.to_str().ok().map(|s| s.to_string())) .unwrap_or_else(|| "".to_owned()); + let subscriber_id = request + .metadata() + .get("x-subscription-id") + .and_then(|h| h.to_str().ok().map(|s| s.to_string())) + .or(request.remote_addr().map(|addr| addr.to_string())); + let config_filter_limits = Arc::clone(&self.config_filter_limits); let filter_names = Arc::clone(&self.filter_names); let incoming_stream_tx = stream_tx.clone(); @@ -1201,6 +1210,7 @@ impl Geyser for GrpcService { tokio::spawn(Self::client_loop( id, + subscriber_id, endpoint, stream_tx, client_rx, diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index 5ef77fef..4d206246 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -67,6 +67,14 @@ lazy_static::lazy_static! { Opts::new("missed_status_message_total", "Number of missed messages by commitment"), &["status"] ).unwrap(); + + static ref GRPC_MESSAGE_SENT: IntCounterVec = IntCounterVec::new( + Opts::new( + "grpc_message_sent_count", + "Number of message sent over grpc to downstream client", + ), + &["remote_id"] + ).unwrap(); } #[derive(Debug)] @@ -199,6 +207,7 @@ impl PrometheusService { register!(CONNECTIONS_TOTAL); register!(SUBSCRIPTIONS_TOTAL); register!(MISSED_STATUS_MESSAGE); + register!(GRPC_MESSAGE_SENT); VERSION .with_label_values(&[ @@ -314,6 +323,12 @@ fn not_found_handler() -> http::Result>> { .body(BodyEmpty::new().boxed()) } +pub fn incr_grpc_message_sent_counter>(remote_id: S) { + GRPC_MESSAGE_SENT + .with_label_values(&[remote_id.as_ref()]) + .inc(); +} + pub fn update_slot_status(status: &GeyserSlosStatus, slot: u64) { SLOT_STATUS .with_label_values(&[status.as_str()]) diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs index a41cb0a5..190d4f92 100644 --- a/yellowstone-grpc-geyser/src/util/ema.rs +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -77,9 +77,9 @@ impl EMAReactivity { /// /// This is how many steps in EMA before the data becomes "irrelevant" to current load computation. /// Which is another way of saying after how many steps the last record will only contribute to 1% of the prediction. - /// + /// /// Log_{alpha}(0.01) - /// + /// const fn effective_memory(self) -> u64 { match self { EMAReactivity::VeryReactive => 7, @@ -151,7 +151,7 @@ impl Ema { // If the time since the last update is bigger than the window, we need to catch up and update the EMA // for each missed update. let missed_updates = time_since_last_update.saturating_sub(1) / self.window.as_millis(); - + // Limit the number of extra updates to the effective memory of the reactivity level. // This is to prevent excessive computation in case of long delays. // This is a trade-off between accuracy and performance. From 6d14f1df746d1497a5c0e161fd6e058efe294672 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Thu, 31 Jul 2025 21:12:45 -0400 Subject: [PATCH 05/20] metrics: geyser_account_update_received --- yellowstone-grpc-geyser/src/grpc.rs | 40 +++++- yellowstone-grpc-geyser/src/metrics.rs | 48 ++++++- yellowstone-grpc-geyser/src/util/ema.rs | 144 +++++++++++++-------- yellowstone-grpc-geyser/src/util/shed.rs | 8 -- yellowstone-grpc-geyser/src/util/stream.rs | 36 ++++-- 5 files changed, 195 insertions(+), 81 deletions(-) delete mode 100644 yellowstone-grpc-geyser/src/util/shed.rs diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index 6229717b..222c0af6 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -1,7 +1,11 @@ use { crate::{ config::{ConfigGrpc, ConfigTokio}, - metrics::{self, DebugClientMessage}, + metrics::{self, set_subscriber_pace, DebugClientMessage}, + util::{ + ema::{Ema, EmaReactivity, DEFAULT_EMA_WINDOW}, + stream::TrafficWeighted, + }, version::GrpcVersionInfo, }, anyhow::Context, @@ -55,6 +59,7 @@ use { IsBlockhashValidResponse, PingRequest, PongResponse, SubscribeReplayInfoRequest, SubscribeReplayInfoResponse, SubscribeRequest, }, + prost::Message as ProstMessage, }, }; @@ -86,6 +91,25 @@ struct BlockMetaStorageInner { finalized: Option, } +impl TrafficWeighted for FilteredUpdate { + fn weight(&self) -> u32 { + self.encoded_len() as u32 + } +} + +impl TrafficWeighted for Result +where + T: TrafficWeighted, + E: TrafficWeighted, +{ + fn weight(&self) -> u32 { + match self { + Ok(item) => item.weight(), + Err(err) => err.weight(), + } + } +} + #[derive(Debug)] struct BlockMetaStorage { read_sem: Semaphore, @@ -650,6 +674,7 @@ impl GrpcService { } // Dedup accounts by max write_version Message::Account(msg) => { + metrics::observe_geyser_account_update_received(msg.account.data.len()); let write_version = msg.account.write_version; let msg_index = slot_messages.messages.len() - 1; if let Some(entry) = slot_messages.accounts_dedup.get_mut(&msg.account.pubkey) { @@ -879,8 +904,17 @@ impl GrpcService { .await; } + let client_loop_pace = Ema::builder() + .window(DEFAULT_EMA_WINDOW) + .reactivity(EmaReactivity::Reactive) + .build(); + if is_alive { 'outer: loop { + set_subscriber_pace( + &subscriber_id, + client_loop_pace.current_load().per_second() as i64, + ); tokio::select! { mut message = client_rx.recv() => { // forward to latest filter @@ -947,9 +981,11 @@ impl GrpcService { messages.sort_by_key(|msg| msg.0); for (_msgid, message) in messages.iter() { for message in filter.get_updates(message, Some(commitment)) { + let proto_size = message.encoded_len().min(u32::MAX as usize) as u32; match stream_tx.send(Ok(message)).await { Ok(()) => { metrics::incr_grpc_message_sent_counter(&subscriber_id); + metrics::incr_grpc_bytes_sent(&subscriber_id, proto_size); } Err(mpsc::error::SendError(_)) => { error!("client #{id}: stream closed"); @@ -983,6 +1019,8 @@ impl GrpcService { } }; + client_loop_pace.record_load(Instant::now().into(), messages.len() as u32); + if commitment == filter.get_commitment_level() { for (_msgid, message) in messages.iter() { for message in filter.get_updates(message, Some(commitment)) { diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index 4d206246..93c2db78 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -12,7 +12,10 @@ use { server::conn::auto::Builder as ServerBuilder, }, log::{error, info}, - prometheus::{IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, TextEncoder}, + prometheus::{ + Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, + TextEncoder, + }, solana_clock::Slot, std::{ collections::{hash_map::Entry as HashMapEntry, HashMap}, @@ -73,7 +76,28 @@ lazy_static::lazy_static! { "grpc_message_sent_count", "Number of message sent over grpc to downstream client", ), - &["remote_id"] + &["subscriber_id"] + ).unwrap(); + + static ref GRPC_BYTES_SENT: IntCounterVec = IntCounterVec::new( + Opts::new("grpc_bytes_sent", "Number of bytes sent over grpc to downstream client"), + &["subscriber_id"] + ).unwrap(); + + static ref GRPC_SUBSCRIBER_MESSAGE_PROCESSING_PACE: IntGaugeVec = IntGaugeVec::new( + Opts::new( + "grpc_subscriber_message_processing_pace_sec", + "How many subscriber loop process incoming geyser message per second" + ), + &["subscriber_id"] + ).unwrap(); + + static ref GEYSER_ACCOUNT_UPDATE_RECEIVED: Histogram = Histogram::with_opts( + HistogramOpts::new( + "geyser_account_update_data_size_kib", + "Histogram of all account update data (kib) received from Geyser plugin" + ) + .buckets(vec![5.0, 10.0, 20.0, 30.0, 50.0, 100.0, 200.0, 300.0, 500.0, 1000.0, 2000.0, 3000.0, 5000.0, 10000.0]) ).unwrap(); } @@ -208,7 +232,9 @@ impl PrometheusService { register!(SUBSCRIPTIONS_TOTAL); register!(MISSED_STATUS_MESSAGE); register!(GRPC_MESSAGE_SENT); - + register!(GRPC_BYTES_SENT); + register!(GRPC_SUBSCRIBER_MESSAGE_PROCESSING_PACE); + register!(GEYSER_ACCOUNT_UPDATE_RECEIVED); VERSION .with_label_values(&[ VERSION_INFO.buildts, @@ -323,6 +349,12 @@ fn not_found_handler() -> http::Result>> { .body(BodyEmpty::new().boxed()) } +pub fn incr_grpc_bytes_sent>(remote_id: S, byte_sent: u32) { + GRPC_BYTES_SENT + .with_label_values(&[remote_id.as_ref()]) + .inc_by(byte_sent as u64); +} + pub fn incr_grpc_message_sent_counter>(remote_id: S) { GRPC_MESSAGE_SENT .with_label_values(&[remote_id.as_ref()]) @@ -385,3 +417,13 @@ pub fn missed_status_message_inc(status: SlotStatus) { .with_label_values(&[status.as_str()]) .inc() } + +pub fn set_subscriber_pace>(subscriber_id: S, pace: i64) { + GRPC_SUBSCRIBER_MESSAGE_PROCESSING_PACE + .with_label_values(&[subscriber_id.as_ref()]) + .set(pace); +} + +pub fn observe_geyser_account_update_received(data_bytesize: usize) { + GEYSER_ACCOUNT_UPDATE_RECEIVED.observe(data_bytesize as f64 / 1024.0); +} diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs index 190d4f92..4bb7c90c 100644 --- a/yellowstone-grpc-geyser/src/util/ema.rs +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -16,16 +16,46 @@ pub struct Ema { window: Duration, window_load: AtomicU64, current_load_ema: AtomicU64, - reactivity: EMAReactivity, + reactivity: EmaReactivity, last_update: RwLock, } +pub struct EmaBuilder { + window: Duration, + reactivity: EmaReactivity, +} + +impl Default for EmaBuilder { + fn default() -> Self { + Self { + window: DEFAULT_EMA_WINDOW, + reactivity: Default::default(), + } + } +} + +impl EmaBuilder { + pub fn window(mut self, window: Duration) -> Self { + self.window = window; + self + } + + pub fn reactivity(mut self, reactivity: EmaReactivity) -> Self { + self.reactivity = reactivity; + self + } + + pub fn build(self) -> Ema { + Ema::new(self.window, self.reactivity) + } +} + /// /// Exponential Moving Average (EMA) reactivity categories. /// /// These categories define how much weight the EMA gives to recent values compared to older ones. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] -pub enum EMAReactivity { +pub enum EmaReactivity { /// Very reactive, attribute 50% weight to the most recent value. VeryReactive, /// Reactive, attribute 33% weight to the most recent value. @@ -37,38 +67,38 @@ pub enum EMAReactivity { LessReactive, } -impl fmt::Display for EMAReactivity { +impl fmt::Display for EmaReactivity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - EMAReactivity::VeryReactive => write!(f, "Very Reactive"), - EMAReactivity::Reactive => write!(f, "Reactive"), - EMAReactivity::ModeratelyReactive => write!(f, "Moderately Reactive"), - EMAReactivity::LessReactive => write!(f, "Less Reactive"), + EmaReactivity::VeryReactive => write!(f, "Very Reactive"), + EmaReactivity::Reactive => write!(f, "Reactive"), + EmaReactivity::ModeratelyReactive => write!(f, "Moderately Reactive"), + EmaReactivity::LessReactive => write!(f, "Less Reactive"), } } } -impl FromStr for EMAReactivity { +impl FromStr for EmaReactivity { type Err = &'static str; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { - "very reactive" => Ok(EMAReactivity::VeryReactive), - "reactive" => Ok(EMAReactivity::Reactive), - "moderately reactive" => Ok(EMAReactivity::ModeratelyReactive), - "less reactive" => Ok(EMAReactivity::LessReactive), + "very reactive" => Ok(EmaReactivity::VeryReactive), + "reactive" => Ok(EmaReactivity::Reactive), + "moderately reactive" => Ok(EmaReactivity::ModeratelyReactive), + "less reactive" => Ok(EmaReactivity::LessReactive), _ => Err("Invalid EMA reactivity"), } } } -impl EMAReactivity { +impl EmaReactivity { const fn as_period(self) -> u64 { match self { - EMAReactivity::VeryReactive => 3, - EMAReactivity::Reactive => 5, - EMAReactivity::ModeratelyReactive => 10, - EMAReactivity::LessReactive => 20, + EmaReactivity::VeryReactive => 3, + EmaReactivity::Reactive => 5, + EmaReactivity::ModeratelyReactive => 10, + EmaReactivity::LessReactive => 20, } } @@ -82,27 +112,27 @@ impl EMAReactivity { /// const fn effective_memory(self) -> u64 { match self { - EMAReactivity::VeryReactive => 7, - EMAReactivity::Reactive => 5, - EMAReactivity::ModeratelyReactive => 3, - EMAReactivity::LessReactive => 2, + EmaReactivity::VeryReactive => 7, + EmaReactivity::Reactive => 5, + EmaReactivity::ModeratelyReactive => 3, + EmaReactivity::LessReactive => 2, } } } #[derive(Debug, Clone)] -pub struct EMACurrentLoad { +pub struct EmaCurrentLoad { ema_load: u64, unit: Duration, } -impl Display for EMACurrentLoad { +impl Display for EmaCurrentLoad { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} / {}ms", self.ema_load, self.unit.as_millis()) } } -impl EMACurrentLoad { +impl EmaCurrentLoad { /// /// Converts the current traffic load native unit to taffic per seconds. /// @@ -115,13 +145,17 @@ impl EMACurrentLoad { pub const DEFAULT_EMA_WINDOW: Duration = Duration::from_millis(10); impl Ema { - pub fn new(window: Duration, reactivity: EMAReactivity) -> Self { + pub fn new(window: Duration, reactivity: EmaReactivity) -> Self { Self::with_starting_time(window, reactivity, Instant::now()) } + pub fn builder() -> EmaBuilder { + EmaBuilder::default() + } + pub const fn with_starting_time( window: Duration, - reactivity: EMAReactivity, + reactivity: EmaReactivity, starting_time: Instant, ) -> Self { Self { @@ -188,10 +222,10 @@ impl Ema { self.current_load_ema.load(Ordering::Relaxed) } - pub fn current_load(&self) -> EMACurrentLoad { + pub fn current_load(&self) -> EmaCurrentLoad { let ema_load = self.current_load_ema_in_native_unit(); let unit = self.window; - EMACurrentLoad { ema_load, unit } + EmaCurrentLoad { ema_load, unit } } fn update_ema_if_needed(&self, now: Instant) { @@ -226,55 +260,55 @@ mod tests { #[test] fn test_emareactivity_from_str() { assert_eq!( - EMAReactivity::from_str("Very Reactive").unwrap(), - EMAReactivity::VeryReactive + EmaReactivity::from_str("Very Reactive").unwrap(), + EmaReactivity::VeryReactive ); assert_eq!( - EMAReactivity::from_str("Reactive").unwrap(), - EMAReactivity::Reactive + EmaReactivity::from_str("Reactive").unwrap(), + EmaReactivity::Reactive ); assert_eq!( - EMAReactivity::from_str("Moderately Reactive").unwrap(), - EMAReactivity::ModeratelyReactive + EmaReactivity::from_str("Moderately Reactive").unwrap(), + EmaReactivity::ModeratelyReactive ); assert_eq!( - EMAReactivity::from_str("Less Reactive").unwrap(), - EMAReactivity::LessReactive + EmaReactivity::from_str("Less Reactive").unwrap(), + EmaReactivity::LessReactive ); - assert!(EMAReactivity::from_str("invalid").is_err()); + assert!(EmaReactivity::from_str("invalid").is_err()); } #[test] fn test_emareactivity_display() { let mut s = String::new(); - write!(&mut s, "{}", EMAReactivity::VeryReactive).unwrap(); + write!(&mut s, "{}", EmaReactivity::VeryReactive).unwrap(); assert_eq!(s, "Very Reactive"); s.clear(); - write!(&mut s, "{}", EMAReactivity::Reactive).unwrap(); + write!(&mut s, "{}", EmaReactivity::Reactive).unwrap(); assert_eq!(s, "Reactive"); s.clear(); - write!(&mut s, "{}", EMAReactivity::ModeratelyReactive).unwrap(); + write!(&mut s, "{}", EmaReactivity::ModeratelyReactive).unwrap(); assert_eq!(s, "Moderately Reactive"); s.clear(); - write!(&mut s, "{}", EMAReactivity::LessReactive).unwrap(); + write!(&mut s, "{}", EmaReactivity::LessReactive).unwrap(); assert_eq!(s, "Less Reactive"); } #[test] fn test_emareactivity_as_period() { - assert_eq!(EMAReactivity::VeryReactive.as_period(), 3); - assert_eq!(EMAReactivity::Reactive.as_period(), 5); - assert_eq!(EMAReactivity::ModeratelyReactive.as_period(), 10); - assert_eq!(EMAReactivity::LessReactive.as_period(), 20); + assert_eq!(EmaReactivity::VeryReactive.as_period(), 3); + assert_eq!(EmaReactivity::Reactive.as_period(), 5); + assert_eq!(EmaReactivity::ModeratelyReactive.as_period(), 10); + assert_eq!(EmaReactivity::LessReactive.as_period(), 20); } #[test] fn test_ema_alpha_for_all_reactivities() { let window = Duration::from_secs(10); - let very = Ema::new(window, EMAReactivity::VeryReactive); - let reactive = Ema::new(window, EMAReactivity::Reactive); - let moderate = Ema::new(window, EMAReactivity::ModeratelyReactive); - let less = Ema::new(window, EMAReactivity::LessReactive); + let very = Ema::new(window, EmaReactivity::VeryReactive); + let reactive = Ema::new(window, EmaReactivity::Reactive); + let moderate = Ema::new(window, EmaReactivity::ModeratelyReactive); + let less = Ema::new(window, EmaReactivity::LessReactive); assert!((very.alpha() - 0.5).abs() < 1e-6); assert!((reactive.alpha() - 0.3333333).abs() < 1e-6); assert!((moderate.alpha() - 0.1818181).abs() < 1e-6); @@ -284,7 +318,7 @@ mod tests { #[test] fn test_ema_function_computation() { let window = Duration::from_secs(10); - let ema = Ema::new(window, EMAReactivity::Reactive); // period = 5, alpha = 0.333... + let ema = Ema::new(window, EmaReactivity::Reactive); // period = 5, alpha = 0.333... // current_ema = 6, recent_load = 12 // expected = alpha * recent_load + (1-alpha) * current_ema let expected = (0.33333333333 * 12.0 + 0.6666667 * 6.0) as u64; @@ -297,9 +331,9 @@ mod tests { #[test] fn test_ema_new_initializes_fields() { let window = Duration::from_secs(10); - let ema = Ema::new(window, EMAReactivity::Reactive); + let ema = Ema::new(window, EmaReactivity::Reactive); assert_eq!(ema.window, window); - assert_eq!(ema.reactivity, EMAReactivity::Reactive); + assert_eq!(ema.reactivity, EmaReactivity::Reactive); assert_eq!(ema.current_load_ema_in_native_unit(), 0); } @@ -307,7 +341,7 @@ mod tests { fn test_record_load_increments_and_updates_ema_after_window() { let now = Instant::now(); const WINDOW: Duration = Duration::from_millis(1); - let ema = Ema::with_starting_time(WINDOW, EMAReactivity::VeryReactive, now); + let ema = Ema::with_starting_time(WINDOW, EmaReactivity::VeryReactive, now); let now_1w = now + WINDOW; ema.record_load(now, 1); ema.record_load(now, 1); @@ -319,7 +353,7 @@ mod tests { fn recent_load_change_should_not_change_last_ema() { let now = Instant::now(); const WINDOW: Duration = Duration::from_millis(1); - let ema = Ema::with_starting_time(WINDOW, EMAReactivity::VeryReactive, now); + let ema = Ema::with_starting_time(WINDOW, EmaReactivity::VeryReactive, now); // Add 3 load in the current EMA window. ema.record_load(now, 1); ema.record_load(now, 1); @@ -340,7 +374,7 @@ mod tests { #[test] fn test_alpha_calculation() { - let ema = Ema::new(Duration::from_secs(10), EMAReactivity::VeryReactive); + let ema = Ema::new(Duration::from_secs(10), EmaReactivity::VeryReactive); let alpha = ema.alpha(); assert!(alpha > 0.0 && alpha < 1.0); } diff --git a/yellowstone-grpc-geyser/src/util/shed.rs b/yellowstone-grpc-geyser/src/util/shed.rs deleted file mode 100644 index b2ef9435..00000000 --- a/yellowstone-grpc-geyser/src/util/shed.rs +++ /dev/null @@ -1,8 +0,0 @@ -use std::time::Instant; - -#[derive(Default)] -pub struct ClientShedder { - first_throttle_hint: Option, -} - -impl ClientShedder {} diff --git a/yellowstone-grpc-geyser/src/util/stream.rs b/yellowstone-grpc-geyser/src/util/stream.rs index 9bb4f64c..1fdac829 100644 --- a/yellowstone-grpc-geyser/src/util/stream.rs +++ b/yellowstone-grpc-geyser/src/util/stream.rs @@ -1,6 +1,6 @@ use { crate::util::{ - ema::{EMACurrentLoad, EMAReactivity, Ema, DEFAULT_EMA_WINDOW}, + ema::{Ema, EmaCurrentLoad, EmaReactivity, DEFAULT_EMA_WINDOW}, rate::RateTracker, }, futures::Stream, @@ -18,7 +18,15 @@ use { }, }; -pub trait Weighted { +/// +/// Basic trait for items that can be sent through a load-aware channel. +/// It requires the item to implement the `weight` method, which returns a `u32` representing the "traffic" weight of the item. +/// This weight is used to track the load on the channel. +/// +/// The term "traffic" is used here to indicate the load or weight of the item being. +/// Its up to the application code to interpret the meaning of "traffic" in the context of the items being sent. +/// +pub trait TrafficWeighted { fn weight(&self) -> u32; } @@ -79,9 +87,9 @@ pub const DEFAULT_AVG_TRAFFIC_RATE_WINDOW: Duration = Duration::from_secs(10 * 6 pub struct StatsSettings { avg_traffic_rate_window: Duration, tx_ema_window: Duration, - tx_ema_reactivity: EMAReactivity, + tx_ema_reactivity: EmaReactivity, rx_ema_window: Duration, - rx_ema_reactivity: EMAReactivity, + rx_ema_reactivity: EmaReactivity, } impl Default for StatsSettings { @@ -89,9 +97,9 @@ impl Default for StatsSettings { Self { avg_traffic_rate_window: DEFAULT_AVG_TRAFFIC_RATE_WINDOW, tx_ema_window: DEFAULT_EMA_WINDOW, - tx_ema_reactivity: EMAReactivity::Reactive, + tx_ema_reactivity: EmaReactivity::Reactive, rx_ema_window: DEFAULT_EMA_WINDOW, - rx_ema_reactivity: EMAReactivity::LessReactive, // Less reactive for receiving end -> closer to an all-time average + rx_ema_reactivity: EmaReactivity::LessReactive, // Less reactive for receiving end -> closer to an all-time average } } } @@ -108,7 +116,7 @@ pub fn load_aware_channel( stats_settings: StatsSettings, ) -> (LoadAwareSender, LoadAwareReceiver) where - T: Weighted, + T: TrafficWeighted, { let (inner_sender, inner_receiver) = tokio::sync::mpsc::channel(capacity); @@ -145,13 +153,13 @@ where /// impl LoadAwareSender where - T: Weighted, + T: TrafficWeighted, { - pub fn estimated_send_rate(&self) -> EMACurrentLoad { + pub fn estimated_send_rate(&self) -> EmaCurrentLoad { self.shared.send_ema.current_load() } - pub fn estimated_consuming_rate(&self) -> EMACurrentLoad { + pub fn estimated_consuming_rate(&self) -> EmaCurrentLoad { self.shared.rx_ema.current_load() } @@ -179,7 +187,7 @@ where /// impl LoadAwareReceiver where - T: Weighted, + T: TrafficWeighted, { pub async fn recv(&mut self) -> Option { use std::future::poll_fn; @@ -197,14 +205,14 @@ where }) } - pub fn estimated_rx_rate(&self) -> EMACurrentLoad { + pub fn estimated_rx_rate(&self) -> EmaCurrentLoad { self.shared.rx_ema.current_load() } } impl Stream for LoadAwareReceiver where - T: Weighted, + T: TrafficWeighted, { type Item = T; @@ -231,7 +239,7 @@ mod tests { #[derive(Debug)] struct TestItem(u32); - impl Weighted for TestItem { + impl TrafficWeighted for TestItem { fn weight(&self) -> u32 { self.0 } From aa5d546c7cdacf84e6b196a69eec0a162e7aed57 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 10:34:54 -0400 Subject: [PATCH 06/20] added load metrics for client_loop --- yellowstone-grpc-geyser/src/grpc.rs | 67 +++++++++++++---- yellowstone-grpc-geyser/src/metrics.rs | 34 ++++++++- yellowstone-grpc-geyser/src/util/ema.rs | 7 ++ yellowstone-grpc-geyser/src/util/stream.rs | 81 ++++++++++----------- yellowstone-grpc-geyser/src/util/testkit.rs | 5 +- 5 files changed, 132 insertions(+), 62 deletions(-) diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index 222c0af6..ec28a4be 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -1,10 +1,16 @@ use { crate::{ config::{ConfigGrpc, ConfigTokio}, - metrics::{self, set_subscriber_pace, DebugClientMessage}, + metrics::{ + self, set_subscriber_pace, set_subscriber_recv_bandwidth_load, + set_subscriber_send_bandwidth_load, DebugClientMessage, + }, util::{ ema::{Ema, EmaReactivity, DEFAULT_EMA_WINDOW}, - stream::TrafficWeighted, + stream::{ + load_aware_channel, LoadAwareReceiver, LoadAwareSender, StatsSettings, + TrafficWeighted, + }, }, version::GrpcVersionInfo, }, @@ -28,7 +34,6 @@ use { task::spawn_blocking, time::{sleep, Duration, Instant}, }, - tokio_stream::wrappers::ReceiverStream, tonic::{ service::interceptor::interceptor, transport::{ @@ -97,6 +102,13 @@ impl TrafficWeighted for FilteredUpdate { } } +impl TrafficWeighted for Status { + fn weight(&self) -> u32 { + // Rough estimate of the size of a Status message, we don't really care about the exact size + self.message().len() as u32 + } +} + impl TrafficWeighted for Result where T: TrafficWeighted, @@ -872,7 +884,7 @@ impl GrpcService { id: usize, subscriber_id: Option, endpoint: String, - stream_tx: mpsc::Sender>, + stream_tx: LoadAwareSender>, mut client_rx: mpsc::UnboundedReceiver, Filter)>>, mut snapshot_rx: Option>>, mut messages_rx: broadcast::Receiver, @@ -895,7 +907,7 @@ impl GrpcService { Self::client_loop_snapshot( id, &endpoint, - &stream_tx, + stream_tx.clone(), &mut client_rx, snapshot_rx, &mut is_alive, @@ -915,6 +927,17 @@ impl GrpcService { &subscriber_id, client_loop_pace.current_load().per_second() as i64, ); + + set_subscriber_send_bandwidth_load( + &subscriber_id, + stream_tx.estimated_send_rate().per_second() as i64, + ); + + set_subscriber_recv_bandwidth_load( + &subscriber_id, + stream_tx.estimated_consuming_rate().per_second() as i64, + ); + tokio::select! { mut message = client_rx.recv() => { // forward to latest filter @@ -1024,8 +1047,12 @@ impl GrpcService { if commitment == filter.get_commitment_level() { for (_msgid, message) in messages.iter() { for message in filter.get_updates(message, Some(commitment)) { + let proto_size = message.encoded_len().min(u32::MAX as usize) as u32; match stream_tx.try_send(Ok(message)) { - Ok(()) => {} + Ok(()) => { + metrics::incr_grpc_message_sent_counter(&subscriber_id); + metrics::incr_grpc_bytes_sent(&subscriber_id, proto_size); + } Err(mpsc::error::TrySendError::Full(_)) => { error!("client #{id}: lagged to send an update"); tokio::spawn(async move { @@ -1040,6 +1067,8 @@ impl GrpcService { } } } + } else { + stream_tx.no_load(); } if commitment == CommitmentLevel::Processed && debug_client_tx.is_some() { @@ -1064,7 +1093,7 @@ impl GrpcService { async fn client_loop_snapshot( id: usize, endpoint: &str, - stream_tx: &mpsc::Sender>, + stream_tx: LoadAwareSender>, client_rx: &mut mpsc::UnboundedReceiver, Filter)>>, snapshot_rx: crossbeam_channel::Receiver>, is_alive: &mut bool, @@ -1127,7 +1156,7 @@ impl GrpcService { #[tonic::async_trait] impl Geyser for GrpcService { - type SubscribeStream = ReceiverStream>; + type SubscribeStream = LoadAwareReceiver>; async fn subscribe( &self, @@ -1141,11 +1170,21 @@ impl Geyser for GrpcService { } else { None }; - let (stream_tx, stream_rx) = mpsc::channel(if snapshot_rx.is_some() { - self.config_snapshot_client_channel_capacity - } else { - self.config_channel_capacity - }); + + let client_stats_settigns = StatsSettings::default() + .tx_ema_reactivity(EmaReactivity::Reactive) + .tx_ema_window(DEFAULT_EMA_WINDOW) + .rx_ema_reactivity(EmaReactivity::Reactive) + .rx_ema_window(DEFAULT_EMA_WINDOW); + + let (stream_tx, stream_rx) = load_aware_channel( + if snapshot_rx.is_some() { + self.config_snapshot_client_channel_capacity + } else { + self.config_channel_capacity + }, + client_stats_settigns, + ); let (client_tx, client_rx) = mpsc::unbounded_channel(); let notify_exit1 = Arc::new(Notify::new()); let notify_exit2 = Arc::new(Notify::new()); @@ -1262,7 +1301,7 @@ impl Geyser for GrpcService { }, )); - Ok(Response::new(ReceiverStream::new(stream_rx))) + Ok(Response::new(stream_rx)) } async fn subscribe_first_available_slot( diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index 93c2db78..fe8677c9 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -13,8 +13,7 @@ use { }, log::{error, info}, prometheus::{ - Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, - TextEncoder, + Histogram, HistogramOpts, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, TextEncoder, }, solana_clock::Slot, std::{ @@ -92,6 +91,22 @@ lazy_static::lazy_static! { &["subscriber_id"] ).unwrap(); + static ref GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD: IntGaugeVec = IntGaugeVec::new( + Opts::new( + "grpc_subscriber_send_bandwidth_load", + "Current Send load we send to subscriber channel (in bytes per second)" + ), + &["subscriber_id"] + ).unwrap(); + + static ref GRPC_SUBCRIBER_RX_LOAD: IntGaugeVec = IntGaugeVec::new( + Opts::new( + "grpc_subscriber_recv_bandwidth_load", + "Current Receive load of subscriber channel (in bytes per second)" + ), + &["subscriber_id"] + ).unwrap(); + static ref GEYSER_ACCOUNT_UPDATE_RECEIVED: Histogram = Histogram::with_opts( HistogramOpts::new( "geyser_account_update_data_size_kib", @@ -235,6 +250,9 @@ impl PrometheusService { register!(GRPC_BYTES_SENT); register!(GRPC_SUBSCRIBER_MESSAGE_PROCESSING_PACE); register!(GEYSER_ACCOUNT_UPDATE_RECEIVED); + register!(GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD); + register!(GRPC_SUBCRIBER_RX_LOAD); + VERSION .with_label_values(&[ VERSION_INFO.buildts, @@ -427,3 +445,15 @@ pub fn set_subscriber_pace>(subscriber_id: S, pace: i64) { pub fn observe_geyser_account_update_received(data_bytesize: usize) { GEYSER_ACCOUNT_UPDATE_RECEIVED.observe(data_bytesize as f64 / 1024.0); } + +pub fn set_subscriber_send_bandwidth_load>(subscriber_id: S, load: i64) { + GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD + .with_label_values(&[subscriber_id.as_ref()]) + .set(load); +} + +pub fn set_subscriber_recv_bandwidth_load>(subscriber_id: S, load: i64) { + GRPC_SUBCRIBER_RX_LOAD + .with_label_values(&[subscriber_id.as_ref()]) + .set(load); +} diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs index 4bb7c90c..2d005789 100644 --- a/yellowstone-grpc-geyser/src/util/ema.rs +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -251,6 +251,13 @@ impl Ema { self.window_load.fetch_add(load as u64, Ordering::Relaxed); self.update_ema_if_needed(now); } + + /// + /// May trigger an EMA update if the last update was more than `window` ago. + /// + pub fn record_no_load(&self, now: Instant) { + self.update_ema_if_needed(now); + } } #[cfg(test)] diff --git a/yellowstone-grpc-geyser/src/util/stream.rs b/yellowstone-grpc-geyser/src/util/stream.rs index 1fdac829..43a10975 100644 --- a/yellowstone-grpc-geyser/src/util/stream.rs +++ b/yellowstone-grpc-geyser/src/util/stream.rs @@ -1,14 +1,8 @@ use { - crate::util::{ - ema::{Ema, EmaCurrentLoad, EmaReactivity, DEFAULT_EMA_WINDOW}, - rate::RateTracker, - }, + crate::util::ema::{Ema, EmaCurrentLoad, EmaReactivity, DEFAULT_EMA_WINDOW}, futures::Stream, std::{ - sync::{ - atomic::AtomicU64, // Removed AtomicU32 - Arc, - }, + sync::Arc, task::{Context, Poll}, time::{Duration, Instant}, }, @@ -32,17 +26,8 @@ pub trait TrafficWeighted { #[derive(Debug)] struct Shared { - size: AtomicU64, - weight: AtomicU64, // Reverted back to AtomicU64 send_ema: Ema, rx_ema: Ema, - avg_weight_rate: RateTracker, -} - -#[derive(Debug, Clone)] -struct ChannelCapacities { - threshold_size: u64, - threshold_weight: u64, } #[derive(Debug, Clone)] @@ -60,42 +45,53 @@ impl Shared { #[inline] fn add_load(&self, weight: u32, now: Instant) { // Kept parameter type as u32 - self.size.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - self.weight - .fetch_add(weight as u64, std::sync::atomic::Ordering::Relaxed); // Cast weight to u64 - self.avg_weight_rate.record(now, weight); // Cast weight to u64 for compatibility self.send_ema.record_load(now, weight); // Cast weight to u64 for compatibility } #[inline] fn decr_load(&self, weight: u32, now: Instant) { // Kept parameter type as u32 - self.size.fetch_sub(1, std::sync::atomic::Ordering::Relaxed); - self.weight - .fetch_sub(weight as u64, std::sync::atomic::Ordering::Relaxed); // Cast weight to u64 self.rx_ema.record_load(now, weight); // Cast weight to u64 for compatibility } } -pub const DEFAULT_AVG_TRAFFIC_RATE_WINDOW: Duration = Duration::from_secs(10 * 60); // 10 minutes - /// /// Settings for the load-aware channel. /// This struct defines the parameters for the average traffic rate window and the EMA settings. /// It allows customization of the channel's behavior regarding load tracking and traffic estimation. /// pub struct StatsSettings { - avg_traffic_rate_window: Duration, tx_ema_window: Duration, tx_ema_reactivity: EmaReactivity, rx_ema_window: Duration, rx_ema_reactivity: EmaReactivity, } +impl StatsSettings { + pub fn tx_ema_window(mut self, window: Duration) -> Self { + self.tx_ema_window = window; + self + } + + pub fn tx_ema_reactivity(mut self, reactivity: EmaReactivity) -> Self { + self.tx_ema_reactivity = reactivity; + self + } + + pub fn rx_ema_window(mut self, window: Duration) -> Self { + self.rx_ema_window = window; + self + } + + pub fn rx_ema_reactivity(mut self, reactivity: EmaReactivity) -> Self { + self.rx_ema_reactivity = reactivity; + self + } +} + impl Default for StatsSettings { fn default() -> Self { Self { - avg_traffic_rate_window: DEFAULT_AVG_TRAFFIC_RATE_WINDOW, tx_ema_window: DEFAULT_EMA_WINDOW, tx_ema_reactivity: EmaReactivity::Reactive, rx_ema_window: DEFAULT_EMA_WINDOW, @@ -121,8 +117,6 @@ where let (inner_sender, inner_receiver) = tokio::sync::mpsc::channel(capacity); let shared = Arc::new(Shared { - size: AtomicU64::new(0), - weight: AtomicU64::new(0), send_ema: Ema::new( stats_settings.tx_ema_window, stats_settings.tx_ema_reactivity, @@ -131,7 +125,6 @@ where stats_settings.rx_ema_window, stats_settings.rx_ema_reactivity, ), - avg_weight_rate: RateTracker::new(stats_settings.avg_traffic_rate_window), }); let sender = LoadAwareSender { shared: Arc::clone(&shared), @@ -178,6 +171,20 @@ where self.shared.add_load(entry_weight, now); Ok(()) } + + /// + /// Updates the internal statistics of the sender with no "traffic" item. + /// This is to account for the fact that the sender is still active and should be considered in load calculations, + /// even if no items are being sent at the moment. + /// + /// This method is useful for maintaining the sender's load statistics without actually sending any items. + /// + /// We need this because some client subscribe to event that rarely send items. + /// + pub fn no_load(&self) { + self.shared.send_ema.record_no_load(Instant::now()); + self.shared.rx_ema.record_no_load(Instant::now()); + } } /// @@ -228,11 +235,7 @@ where #[cfg(test)] mod tests { use { - super::*, - crate::util::testkit, - log::LevelFilter, - std::sync::atomic::Ordering, - tokio::{sync::mpsc, task::yield_now}, + super::*, crate::util::testkit, log::LevelFilter, tokio::task::yield_now, tokio_stream::StreamExt, }; @@ -258,14 +261,8 @@ mod tests { #[tokio::test] async fn test_load_tracking() { let (sender, mut receiver) = load_aware_channel(10, Default::default()); - sender.send(TestItem(5)).await.unwrap(); - assert_eq!(sender.shared.size.load(Ordering::Relaxed), 1); - assert_eq!(sender.shared.weight.load(Ordering::Relaxed), 5); - receiver.recv().await.unwrap(); - assert_eq!(sender.shared.size.load(Ordering::Relaxed), 0); - assert_eq!(sender.shared.weight.load(Ordering::Relaxed), 0); } #[tokio::test] diff --git a/yellowstone-grpc-geyser/src/util/testkit.rs b/yellowstone-grpc-geyser/src/util/testkit.rs index db2f7ebf..e1452d7e 100644 --- a/yellowstone-grpc-geyser/src/util/testkit.rs +++ b/yellowstone-grpc-geyser/src/util/testkit.rs @@ -1,7 +1,4 @@ -use { - core::panic, - log::{Level, Metadata, Record}, -}; +use log::{Metadata, Record}; /// /// A simple logger that logs messages to the console. From 01834b393e62de4c9759ac68cffdcb98348f0f9e Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 10:43:01 -0400 Subject: [PATCH 07/20] first draft finish --- examples/rust/src/bin/client.rs | 1 - yellowstone-grpc-geyser/src/util/ema.rs | 2 +- yellowstone-grpc-geyser/src/util/mod.rs | 2 +- yellowstone-grpc-geyser/src/util/rate.rs | 382 --------------------- yellowstone-grpc-geyser/src/util/stream.rs | 8 +- 5 files changed, 6 insertions(+), 389 deletions(-) delete mode 100644 yellowstone-grpc-geyser/src/util/rate.rs diff --git a/examples/rust/src/bin/client.rs b/examples/rust/src/bin/client.rs index d391402f..54b91924 100644 --- a/examples/rust/src/bin/client.rs +++ b/examples/rust/src/bin/client.rs @@ -650,7 +650,6 @@ async fn main() -> anyhow::Result<()> { .inspect_err(|error| error!("failed to connect: {error}")) }) .await - .map_err(Into::into) } async fn geyser_health_watch(mut client: GeyserGrpcClient) -> anyhow::Result<()> { diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs index 2d005789..1e88ed45 100644 --- a/yellowstone-grpc-geyser/src/util/ema.rs +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -190,7 +190,7 @@ impl Ema { // This is to prevent excessive computation in case of long delays. // This is a trade-off between accuracy and performance. // EMA terms forms a geometric series, where each term contributes less and less to the final value. - // The contribution value of each decay exponentially decreases. + // The contribution value of each term decay exponentially. // after the effective memory steps, the contribution of the last record is less than 1% of the prediction. let extra_updates = missed_updates.min(self.reactivity.effective_memory() as u128); diff --git a/yellowstone-grpc-geyser/src/util/mod.rs b/yellowstone-grpc-geyser/src/util/mod.rs index b0dc6c6c..bbe332a9 100644 --- a/yellowstone-grpc-geyser/src/util/mod.rs +++ b/yellowstone-grpc-geyser/src/util/mod.rs @@ -1,5 +1,5 @@ pub mod ema; -pub mod rate; +// pub mod rate; pub mod stream; #[cfg(test)] pub(crate) mod testkit; diff --git a/yellowstone-grpc-geyser/src/util/rate.rs b/yellowstone-grpc-geyser/src/util/rate.rs deleted file mode 100644 index e90458b7..00000000 --- a/yellowstone-grpc-geyser/src/util/rate.rs +++ /dev/null @@ -1,382 +0,0 @@ -use std::{ - fmt, - sync::atomic::{AtomicU64, Ordering}, - time::{Duration, Instant}, -}; - -struct Bin { - timestamp: AtomicU64, // seconds since start_time - sum: AtomicU64, - sum_of_squares: AtomicU64, // Sum of squares for variance calculation -} - -/// -/// Tracks the rate of traffic per second over a specified time window. -/// -pub struct RateTracker { - bins: Box<[Bin]>, - start_time: Instant, - window: Duration, // e.g., 1 hour -} - -impl fmt::Debug for RateTracker { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("RateTracker") - .field("bins", &self.bins.len()) - .field("start_time", &self.start_time) - .field("window", &self.window) - .finish() - } -} - -impl RateTracker { - pub fn new(window: Duration) -> Self { - let now = Instant::now(); - Self::with_starting_time(window, now) - } - - pub fn with_starting_time(window: Duration, now: Instant) -> Self { - let bins = (0..window.as_secs()) - .map(|_| Bin { - timestamp: AtomicU64::new(0), - sum: AtomicU64::new(0), - sum_of_squares: AtomicU64::new(0), - }) - .collect::>() - .into_boxed_slice(); - - Self { - bins, - start_time: now, - window, - } - } - - pub fn record(&self, now: Instant, traffic: u32) { - if now < self.start_time { - return; // Ignore records before the start time - } - let now_secs = now.duration_since(self.start_time).as_secs(); - let bin_index = (now_secs % self.bins.len() as u64) as usize; - let bin = &self.bins[bin_index]; - let ts = bin.timestamp.load(Ordering::Acquire); - if ts != now_secs { - // Try to update timestamp to current second. - // Use AcqRel on success to synchronize with count reset, - // Acquire on failure to see fresh state. - if bin - .timestamp - .compare_exchange(ts, now_secs, Ordering::AcqRel, Ordering::Acquire) - .is_ok() - { - // This thread won the race; clear the count. - // Use Release ordering to ensure the store happens after timestamp update. - bin.sum.store(0, Ordering::Release); - bin.sum_of_squares.store(0, Ordering::Release); - } - // If CAS failed, another thread updated timestamp & count; do nothing. - } - // Increment count atomically. Relaxed ordering is fine here. - bin.sum.fetch_add(traffic as u64, Ordering::Relaxed); - bin.sum_of_squares - .fetch_add((traffic as u64) * (traffic as u64), Ordering::Relaxed); - } - - pub fn average_rate(&self, now: Instant) -> f64 { - if now < self.start_time { - return 0.0; // No events recorded yet - } - let now_secs = now.duration_since(self.start_time).as_secs(); - let min_ts = now_secs.saturating_sub(self.window.as_secs()); - let max_bin = self.bins.len().min(now_secs as usize); - let mut sum = 0.0; - for bin in self.bins.iter().take(max_bin) { - let ts = bin.timestamp.load(Ordering::Relaxed); - if ts < min_ts { - continue; // Skip bins outside the window - } - let bin_sum = bin.sum.load(Ordering::Relaxed); - sum += bin_sum as f64; - } - if (max_bin as f64) == 0.0 { - return 0.0; // No bins to average - } - sum / max_bin as f64 - } - - pub fn variance(&self, now: Instant) -> f64 { - if now < self.start_time { - return 0.0; // No events recorded yet - } - let now_secs = now.duration_since(self.start_time).as_secs(); - let min_ts = now_secs.saturating_sub(self.window.as_secs()); - let max_bin = self.bins.len().min(now_secs as usize); - - if max_bin == 0 { - return 0.0; // No bins to calculate variance - } - - let mut sum = 0u64; - let mut sum_of_squares = 0u64; - - for bin in self.bins.iter().take(max_bin) { - let bin_sum = bin.sum.load(Ordering::Relaxed); - let sq = bin.sum_of_squares.load(Ordering::Relaxed); - let ts = bin.timestamp.load(Ordering::Relaxed); - if ts < min_ts { - continue; // Skip bins outside the window - } - sum += bin_sum; - sum_of_squares += sq; - } - - let mean = sum as f64 / max_bin as f64; - let mean_of_squares = sum_of_squares as f64 / max_bin as f64; - mean_of_squares - (mean * mean) - } - - pub fn stddev(&self, now: Instant) -> f64 { - self.variance(now).sqrt() - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - std::time::{Duration, Instant}, - }; - - #[test] - fn test_rate_tracker_initialization() { - let window = Duration::from_secs(10); - let tracker = RateTracker::new(window); - assert_eq!(tracker.bins.len(), 10); - assert_eq!(tracker.window, window); - } - - #[test] - fn test_no_recorded_traffic() { - let now = Instant::now(); - let window = Duration::from_secs(10); - let tracker = RateTracker::with_starting_time(window, now); - - let rate = tracker.average_rate(now + Duration::from_secs(10)); - let variance = tracker.variance(now + Duration::from_secs(10)); - let stddev = tracker.stddev(now + Duration::from_secs(10)); - - assert_eq!(rate, 0.0); - assert_eq!(variance, 0.0); - assert_eq!(stddev, 0.0); - } - - #[test] - fn test_record_increments_count() { - let now = Instant::now(); - let window = Duration::from_secs(5); - let tracker = RateTracker::with_starting_time(window, now); - - tracker.record(now, 1); - tracker.record(now, 1); - - let now_secs = Instant::now().duration_since(tracker.start_time).as_secs(); - let bin_index = (now_secs % tracker.bins.len() as u64) as usize; - let bin = &tracker.bins[bin_index]; - - assert_eq!(bin.sum.load(Ordering::Relaxed), 2); - } - - #[test] - fn test_average_rate_calculation() { - let now = Instant::now(); - let window = Duration::from_secs(5); - let tracker = RateTracker::with_starting_time(window, now); - - // Simulate recording events - let now_1s = now + Duration::from_secs(1); - let now_2s = now + Duration::from_secs(2); - tracker.record(now, 1); - tracker.record(now, 1); - tracker.record(now_1s, 1); - - // (2 + 1) / 2 = 1.5 - let rate = tracker.average_rate(now_2s); - assert_eq!(rate, 1.5); - } - - #[test] - fn test_variance_and_stddev_calculation() { - let now = Instant::now(); - let window = Duration::from_secs(5); - let tracker = RateTracker::with_starting_time(window, now); - - // Simulate recording events - let now_1s = now + Duration::from_secs(1); - let now_2s = now + Duration::from_secs(2); - let now_3s = now + Duration::from_secs(3); - tracker.record(now, 5); - tracker.record(now_1s, 10); - tracker.record(now_2s, 15); - - let variance = tracker.variance(now_3s); - let stddev = tracker.stddev(now_3s); - - assert!(variance >= 16.6 && variance <= 16.7); // Variance should be around 16.67 - assert!((stddev * stddev - variance).abs() < 1e-10); - } - - #[test] - fn test_multiple_bins_update() { - let now = Instant::now(); - let window = Duration::from_secs(10); - let tracker = RateTracker::with_starting_time(window, now); - - // Simulate events across multiple bins - for i in 0..10 { - let event_time = now + Duration::from_secs(i); - tracker.record(event_time, (i as u32) + 1); // Cast to u32 - } - - // Ensure all bins are within the window by advancing the time - let end_time = now + Duration::from_secs(10); - - let rate = tracker.average_rate(end_time); - let variance = tracker.variance(end_time); - let stddev = tracker.stddev(end_time); - - assert_eq!(rate, 5.5); // Average of 1 through 10 - assert!(variance > 0.0); - assert!(stddev > 0.0); - } - - #[test] - fn test_no_events_recorded() { - let now = Instant::now(); - let window = Duration::from_secs(10); - let tracker = RateTracker::with_starting_time(window, now); - - let rate = tracker.average_rate(now + Duration::from_secs(10)); - let variance = tracker.variance(now + Duration::from_secs(10)); - let stddev = tracker.stddev(now + Duration::from_secs(10)); - - assert_eq!(rate, 0.0); - assert_eq!(variance, 0.0); - assert_eq!(stddev, 0.0); - } - - #[test] - fn test_all_events_outside_window() { - let now = Instant::now(); - let window = Duration::from_secs(10); - let tracker = RateTracker::with_starting_time(window, now); - - // Record events outside the window - tracker.record(now - Duration::from_secs(20), 5 as u32); // Cast to u32 - tracker.record(now - Duration::from_secs(15), 10 as u32); // Cast to u32 - - let rate = tracker.average_rate(now); - let variance = tracker.variance(now); - let stddev = tracker.stddev(now); - - assert_eq!(rate, 0.0); - assert_eq!(variance, 0.0); - assert_eq!(stddev, 0.0); - } - - #[test] - fn test_no_variance() { - let now = Instant::now(); - let window = Duration::from_secs(10); - let tracker = RateTracker::with_starting_time(window, now); - let now_1s = now + Duration::from_secs(1); - let now_2s = now + Duration::from_secs(2); - // Simulate a high volume of events in a single bin - tracker.record(now, 1_000_000 as u32); // Cast to u32 - tracker.record(now_1s, 1_000_000 as u32); // Cast to u32 - - let rate = tracker.average_rate(now_2s); - let variance = tracker.variance(now_2s); - let stddev = tracker.stddev(now_2s); - - assert_eq!(rate, 1_000_000.0); - assert!(variance == 0.0); - assert!(stddev == 0.0); - } - - #[test] - fn test_rate_cyclicity() { - let now = Instant::now(); - let window = Duration::from_secs(3600); // 1 hour - let tracker = RateTracker::with_starting_time(window, now); - let end_first_period = now + Duration::from_secs(3600); - let end_second_period = now + Duration::from_secs(7200); - for i in 0..3600 { - let event_time = now + Duration::from_secs(i); - tracker.record(event_time, i as u32 + 1); - } - // Sum of 1 to 3600 = 6_481_800 - let rate = tracker.average_rate(end_first_period); - assert_eq!(rate, 6_481_800.0 / 3600.0); - - for i in 3600..7200 { - let event_time = now + Duration::from_secs(i); - tracker.record(event_time, i as u32); - } - // Sum of 3660 to 7199 = 19_438_200 - let rate = tracker.average_rate(end_second_period); - assert_eq!(rate, 19_438_200.0 / 3600.0); - - let variance = tracker.variance(end_second_period); - let stddev = tracker.stddev(end_second_period); - assert!(variance > 0.0); - assert!((stddev * stddev - variance).abs() < 1e-5); - } - - #[test] - fn test_long_period_of_inactivity() { - let now = Instant::now(); - let window = Duration::from_secs(3600); // 1 hour - let tracker = RateTracker::with_starting_time(window, now); - let end_first_period = now + Duration::from_secs(3600); - for i in 0..3600 { - let event_time = now + Duration::from_secs(i); - tracker.record(event_time, i as u32 + 1); - } - - let rate = tracker.average_rate(end_first_period); - assert_eq!(rate, 6_481_800.0 / 3600.0); - - // Simulate a long period of inactivity - let long_inactivity = now + Duration::from_secs(7200); - let rate = tracker.average_rate(long_inactivity); - let var = tracker.variance(long_inactivity); - let stddev = tracker.stddev(long_inactivity); - assert_eq!(rate, 0.0); // No events recorded in the second hour - assert_eq!(var, 0.0); // Variance should also be zero - assert_eq!(stddev, 0.0); // Standard deviation should also be zero - assert!((stddev * stddev - var).abs() < 1e-10); - } - - #[test] - fn it_should_handle_holes_in_history() { - let now = Instant::now(); - let window = Duration::from_secs(10); - let tracker = RateTracker::with_starting_time(window, now); - let one_sec = Duration::from_secs(1); - // Record events at 1s and 3s, skipping 2s - tracker.record(now, 1); // 0th second - tracker.record(now + one_sec, 2); // 1st second - // 2nd second is skipped - tracker.record(now + (one_sec * 3), 4); // 3rd second - - // Average rate should consider only the recorded events - let expected_avg = (1 + 2 + 0 + 4) as f64 / 4.0; - let rate = tracker.average_rate(now + Duration::from_secs(4)); - assert_eq!(rate, expected_avg); - - let variance = tracker.variance(now + Duration::from_secs(4)); - let stddev = tracker.stddev(now + Duration::from_secs(4)); - assert_eq!(variance, 2.1875); - assert!((stddev * stddev - variance).abs() < 1e-10); - } -} diff --git a/yellowstone-grpc-geyser/src/util/stream.rs b/yellowstone-grpc-geyser/src/util/stream.rs index 43a10975..94ec94ad 100644 --- a/yellowstone-grpc-geyser/src/util/stream.rs +++ b/yellowstone-grpc-geyser/src/util/stream.rs @@ -68,22 +68,22 @@ pub struct StatsSettings { } impl StatsSettings { - pub fn tx_ema_window(mut self, window: Duration) -> Self { + pub const fn tx_ema_window(mut self, window: Duration) -> Self { self.tx_ema_window = window; self } - pub fn tx_ema_reactivity(mut self, reactivity: EmaReactivity) -> Self { + pub const fn tx_ema_reactivity(mut self, reactivity: EmaReactivity) -> Self { self.tx_ema_reactivity = reactivity; self } - pub fn rx_ema_window(mut self, window: Duration) -> Self { + pub const fn rx_ema_window(mut self, window: Duration) -> Self { self.rx_ema_window = window; self } - pub fn rx_ema_reactivity(mut self, reactivity: EmaReactivity) -> Self { + pub const fn rx_ema_reactivity(mut self, reactivity: EmaReactivity) -> Self { self.rx_ema_reactivity = reactivity; self } From 268cefcf5a338cbaadd6aea9b1b243da83ad765e Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 11:02:26 -0400 Subject: [PATCH 08/20] added reference to anza --- yellowstone-grpc-geyser/src/util/ema.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs index 1e88ed45..2d2cfc87 100644 --- a/yellowstone-grpc-geyser/src/util/ema.rs +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -10,6 +10,7 @@ use std::{ /// /// Exponential Moving Average (EMA) for load tracking. +/// The implementation is based off Anza stream-throttle module: https://github.com/anza-xyz/agave/blob/v2.3/streamer/src/nonblocking/stream_throttle.rs /// #[derive(Debug)] pub struct Ema { From 286a6400b02599b152fd6bd56d1749c5f6dabc53 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 11:03:11 -0400 Subject: [PATCH 09/20] fix clippy --- yellowstone-grpc-geyser/src/util/ema.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yellowstone-grpc-geyser/src/util/ema.rs b/yellowstone-grpc-geyser/src/util/ema.rs index 2d2cfc87..849ba91e 100644 --- a/yellowstone-grpc-geyser/src/util/ema.rs +++ b/yellowstone-grpc-geyser/src/util/ema.rs @@ -36,12 +36,12 @@ impl Default for EmaBuilder { } impl EmaBuilder { - pub fn window(mut self, window: Duration) -> Self { + pub const fn window(mut self, window: Duration) -> Self { self.window = window; self } - pub fn reactivity(mut self, reactivity: EmaReactivity) -> Self { + pub const fn reactivity(mut self, reactivity: EmaReactivity) -> Self { self.reactivity = reactivity; self } From 2b9f1e5115d666c5ba4e68d9587b25378a41c880 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 11:32:26 -0400 Subject: [PATCH 10/20] remove broken clippy wasm step --- .github/workflows/test.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f5237fda..5d4be5ef 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -112,10 +112,6 @@ jobs: - name: cargo clippy run: cargo clippy --workspace --all-targets - - name: cargo clippy wasm - run: | - cd yellowstone-grpc-client-nodejs/solana-encoding-wasm - cargo clippy --target wasm32-unknown-unknown --all-targets - name: check features in `client` run: cargo check -p yellowstone-grpc-client --all-targets From 825827f4ea1a0cc687fd381289527cdd6535f018 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 12:23:56 -0400 Subject: [PATCH 11/20] remove ubuntu 20.04 from build matrix --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0be58693..58bb3a2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: release: strategy: matrix: - os: [ubuntu-20.04, ubuntu-22.04] + os: [ubuntu-22.04] runs-on: ["${{ matrix.os }}"] steps: - name: Maximize build space diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d4be5ef..e4f3aa5d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: test: strategy: matrix: - os: [ubuntu-20.04, ubuntu-22.04] + os: [ubuntu-22.04] runs-on: ["${{ matrix.os }}"] steps: - name: Maximize build space @@ -113,6 +113,12 @@ jobs: - name: cargo clippy run: cargo clippy --workspace --all-targets + # THIS STEP IS BROKEN + # - name: cargo clippy wasm + # run: | + # cd yellowstone-grpc-client-nodejs/solana-encoding-wasm + # cargo clippy --target wasm32-unknown-unknown --all-targets + - name: check features in `client` run: cargo check -p yellowstone-grpc-client --all-targets - name: check features in `client-simple` From edf7117b2d8c72283341caa93a64b79f721a99cf Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 12:27:46 -0400 Subject: [PATCH 12/20] update Cargo.toml --- Cargo.lock | 170 ++++++++++++++++++++++++++++++----------------------- Cargo.toml | 8 +-- 2 files changed, 101 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68d7dcc0..84176842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,9 +77,9 @@ dependencies = [ [[package]] name = "agave-feature-set" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e37f8cd072eae70213ca32490fd957d1206baf738066636ef172b97fba8cd88" +checksum = "d8e4bb8842e634f00f7f56bed7fcf67464bf2689378b3977350a8d0e6918a1ea" dependencies = [ "ahash", "solana-epoch-schedule", @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "agave-geyser-plugin-interface" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52b47b27d6d12070cf4f3d5047baba9e24ea751e06d529e6fc094a0267cc1d2" +checksum = "beba2316745c18031252bae23fc9f8be60933101f7bb8812773f803c699e44e1" dependencies = [ "log", "solana-clock", @@ -105,9 +105,9 @@ dependencies = [ [[package]] name = "agave-reserved-account-keys" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60ea327ab5fd481d946c379168733cc6cecf3fcef045dd344a97255212efda6f" +checksum = "2343e5e83d2a33f965dd2fd18840351d821de9a5a427880a05069cc60ec18a81" dependencies = [ "agave-feature-set", "solana-pubkey", @@ -529,9 +529,9 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.9.3" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ecc273b49b3205b83d648f0690daa588925572cc5063745bfe547fe7ec8e1a1" +checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" dependencies = [ "proc-macro2", "quote", @@ -570,9 +570,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" dependencies = [ "jobserver", "libc", @@ -644,9 +644,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.40" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882" dependencies = [ "clap_builder", "clap_derive", @@ -654,9 +654,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966" dependencies = [ "anstream", "anstyle", @@ -666,9 +666,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -754,9 +754,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -1340,9 +1340,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -1548,9 +1548,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "bytes", "futures-channel", @@ -1561,7 +1561,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -1755,6 +1755,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -2355,9 +2366,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", "syn 2.0.104", @@ -2459,7 +2470,7 @@ dependencies = [ "multimap 0.10.1", "once_cell", "petgraph 0.7.1", - "prettyplease 0.2.35", + "prettyplease 0.2.36", "prost 0.13.5", "prost-types 0.13.5", "regex", @@ -2643,9 +2654,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] @@ -2695,9 +2706,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc_version" @@ -2723,22 +2734,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "log", "once_cell", @@ -2781,9 +2792,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -2898,9 +2909,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -3004,6 +3015,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "solana-account" version = "2.2.1" @@ -3024,9 +3045,9 @@ dependencies = [ [[package]] name = "solana-account-decoder" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5cdf1ddff9738edaa11a8c249ed3f4767fcfa799b0e3e550b8ee2d5550ca9bb" +checksum = "2368a6ff4b9077501a13dca5b409947757c7b3690213c5cd5bd513a89bf343f1" dependencies = [ "Inflector", "base64 0.22.1", @@ -3067,9 +3088,9 @@ dependencies = [ [[package]] name = "solana-account-decoder-client-types" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ccc2edc7ba4cbdcc0e5c0099cae5253d2d2d3aa83612d5d324526c128c43d93" +checksum = "84fe163318f8531029ec3e1796f55f16b61dcf194d9502076cbe3505a815d9d5" dependencies = [ "base64 0.22.1", "bs58", @@ -3212,9 +3233,9 @@ dependencies = [ [[package]] name = "solana-curve25519" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10aa190d9f47432e255edd9aaddd21aa78c31be0e639c8463427d0fe5e6919a0" +checksum = "ad6269c8dded5d571c75a4a32997514f57f23757f2e18549ca3040586465e336" dependencies = [ "bytemuck", "bytemuck_derive", @@ -3807,9 +3828,9 @@ dependencies = [ [[package]] name = "solana-sha256-hasher" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0037386961c0d633421f53560ad7c80675c0447cba4d1bb66d60974dd486c7ea" +checksum = "5aa3feb32c28765f6aa1ce8f3feac30936f16c5c3f7eb73d63a5b8f6f8ecdc44" dependencies = [ "sha2 0.10.9", "solana-define-syscall", @@ -3909,9 +3930,9 @@ dependencies = [ [[package]] name = "solana-storage-proto" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869925e70e37d28b450e6348354970e56b79b99e9e52ce485c1fdc060375259b" +checksum = "db22405bea8cfee4c883c254e22a191bc3a540dcee15f4cd6ce06ec20d1ce32f" dependencies = [ "bincode", "bs58", @@ -3934,9 +3955,9 @@ dependencies = [ [[package]] name = "solana-svm-feature-set" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee75d3a0d7ee9c3391bb76f7e83ca6623ae08f199c13236595d5e5d0fa760d51" +checksum = "d4d525b3bb05c5a56c17ec7f4d5b9f838f0bcf006cf423a7c0e1b05ef4e10a2a" [[package]] name = "solana-system-interface" @@ -4028,9 +4049,9 @@ dependencies = [ [[package]] name = "solana-transaction-context" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e6d545a650d488b9af4edb1f278f6dabb58189be3d956bc992151b01ac8ab5" +checksum = "62899fc8ec399458db3332ec51dfc8379ca8bb5615133510c8b4dca4c5d48111" dependencies = [ "bincode", "serde", @@ -4057,9 +4078,9 @@ dependencies = [ [[package]] name = "solana-transaction-status" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773b99fbea5542311a507cb0c98f5eb5c325c887fa519215dfe0b924ba474cb7" +checksum = "0d824edb8ca9fd7b35499c88709ea4a4d5c74a765e5e1912ee6d8b9964265e82" dependencies = [ "Inflector", "agave-reserved-account-keys", @@ -4101,9 +4122,9 @@ dependencies = [ [[package]] name = "solana-transaction-status-client-types" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a683033f882aea3e9fdf98087e0aa3bc3019f923430d26fe4e66452ebdaaf69" +checksum = "2d10a5585f489ca00b0d9fdfd4ffb159450facca7ce52ca025268975f74013c8" dependencies = [ "base64 0.22.1", "bincode", @@ -4124,9 +4145,9 @@ dependencies = [ [[package]] name = "solana-vote-interface" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f08746f154458f28b98330c0d55cb431e2de64ee4b8efc98dcbe292e0672b" +checksum = "b80d57478d6599d30acc31cc5ae7f93ec2361a06aefe8ea79bc81739a08af4c3" dependencies = [ "bincode", "num-derive", @@ -4148,9 +4169,9 @@ dependencies = [ [[package]] name = "solana-zk-sdk" -version = "2.3.2" +version = "2.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1956de8311683b1adef202afee3b49d90d476505db47c979511dc53436cf640c" +checksum = "05857892ac50fe03c125d8445fd790c6768015b76f4ad1e4b4b1499938b357f0" dependencies = [ "aes-gcm-siv", "base64 0.22.1", @@ -4624,7 +4645,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.7", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -4747,18 +4768,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.45.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "pin-project-lite", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4880,7 +4903,7 @@ dependencies = [ "prost 0.13.5", "rustls-native-certs", "rustls-pemfile", - "socket2", + "socket2 0.5.10", "tokio", "tokio-rustls", "tokio-stream", @@ -4910,7 +4933,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9557ce109ea773b399c9b9e5dca39294110b74f1f342cb347a80d1fce8c26a11" dependencies = [ - "prettyplease 0.2.35", + "prettyplease 0.2.36", "proc-macro2", "prost-build 0.13.5", "prost-types 0.13.5", @@ -5356,7 +5379,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -5377,10 +5400,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -5489,9 +5513,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index f707595d..5944bab3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,11 +70,11 @@ solana-transaction = "~2.2.1" solana-transaction-context = "~2.3.0" solana-transaction-error = "~2.2.1" solana-transaction-status = "~2.3.0" -smallvec = "1.13.2" +smallvec = "1" spl-token-2022 = "8.0.0" -thiserror = "1.0.63" -tokio = "1.21.2" -tokio-stream = "0.1.11" +thiserror = "1" +tokio = "1" +tokio-stream = "~0.1.11" tonic = "0.12.1" tonic-build = "0.12.1" tonic-health = "0.12.1" From d1a3ba7c4f2daf645f5fa306ff2a6ef2bc0b8c93 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 13:31:28 -0400 Subject: [PATCH 13/20] updated metric name --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- yellowstone-grpc-geyser/src/metrics.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 58bb3a2d..2133aee8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,7 @@ jobs: release: strategy: matrix: - os: [ubuntu-22.04] + os: [ubuntu-22.04, ubuntu-24.04] runs-on: ["${{ matrix.os }}"] steps: - name: Maximize build space diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e4f3aa5d..b4ee8a1c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,7 @@ jobs: test: strategy: matrix: - os: [ubuntu-22.04] + os: [ubuntu-22.04, ubuntu-24.04] runs-on: ["${{ matrix.os }}"] steps: - name: Maximize build space diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index fe8677c9..9e5dc9c7 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -102,7 +102,7 @@ lazy_static::lazy_static! { static ref GRPC_SUBCRIBER_RX_LOAD: IntGaugeVec = IntGaugeVec::new( Opts::new( "grpc_subscriber_recv_bandwidth_load", - "Current Receive load of subscriber channel (in bytes per second)" + "Current Receiver rate of subscriber channel (in bytes per second)" ), &["subscriber_id"] ).unwrap(); From 3f68fad4dd731d811f614f18eba3b5917330ee8c Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 13:36:32 -0400 Subject: [PATCH 14/20] set gauge to 0 --- yellowstone-grpc-geyser/src/grpc.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index ec28a4be..443026b2 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -1082,6 +1082,9 @@ impl GrpcService { } } } + set_subscriber_recv_bandwidth_load(&subscriber_id, 0); + set_subscriber_send_bandwidth_load(&subscriber_id, 0); + set_subscriber_pace(&subscriber_id, 0); metrics::connections_total_dec(); DebugClientMessage::maybe_send(&debug_client_tx, || DebugClientMessage::Removed { id }); From c70500f40d83aa0fe704fdacecde1c6ce5d17ba4 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 13:52:29 -0400 Subject: [PATCH 15/20] CHANGELOG + bump to geyser v8.1.0 --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- yellowstone-grpc-geyser/Cargo.toml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa66ace..1e69928b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,15 @@ The minor version will be incremented upon a breaking change and the patch versi ### Breaking +## 2025-07-01 + +- yellowstone-grpc-geyser-8.1.0 + +### Features + +- geyser plugin exposes new metrics to measure subscriber performance such send/recv bandwidth load. +- Added metrics to measure the amount of account update and sizes we are receiving from agave. + ## 2025-06-30 - @triton-one/yellowstone-grpc@4.1.0 diff --git a/Cargo.lock b/Cargo.lock index 84176842..d37fc895 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5577,7 +5577,7 @@ dependencies = [ [[package]] name = "yellowstone-grpc-geyser" -version = "8.0.0" +version = "8.1.0" dependencies = [ "affinity", "agave-geyser-plugin-interface", diff --git a/yellowstone-grpc-geyser/Cargo.toml b/yellowstone-grpc-geyser/Cargo.toml index 99027857..2fc093a6 100644 --- a/yellowstone-grpc-geyser/Cargo.toml +++ b/yellowstone-grpc-geyser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yellowstone-grpc-geyser" -version = "8.0.0" +version = "8.1.0" authors = { workspace = true } edition = { workspace = true } description = "Yellowstone gRPC Geyser Plugin" From e5ca419f12ed5041dfc8b1f05521fc585746fffa Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 15:34:44 -0400 Subject: [PATCH 16/20] client: support compression --- examples/rust/Cargo.toml | 2 +- examples/rust/src/bin/client.rs | 36 +++++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index 61af8d99..e5461b94 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -31,7 +31,7 @@ solana-pubkey = { workspace = true } solana-signature = { workspace = true } solana-transaction-status = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "fs"] } -tonic = { workspace = true } +tonic = { workspace = true, features = ["zstd", "gzip"] } yellowstone-grpc-client = { workspace = true } yellowstone-grpc-proto = { workspace = true, features = ["plugin"] } diff --git a/examples/rust/src/bin/client.rs b/examples/rust/src/bin/client.rs index 54b91924..f6ddb846 100644 --- a/examples/rust/src/bin/client.rs +++ b/examples/rust/src/bin/client.rs @@ -11,12 +11,7 @@ use { solana_signature::Signature, solana_transaction_status::UiTransactionEncoding, std::{ - collections::HashMap, - env, - fs::File, - path::PathBuf, - sync::Arc, - time::{Duration, Instant, SystemTime, UNIX_EPOCH}, + collections::HashMap, env, fs::File, path::PathBuf, str::FromStr, sync::Arc, time::{Duration, Instant, SystemTime, UNIX_EPOCH} }, tokio::{fs, sync::Mutex}, tonic::transport::{channel::ClientTlsConfig, Certificate}, @@ -49,6 +44,24 @@ type EntryFilterMap = HashMap; type BlocksFilterMap = HashMap; type BlocksMetaFilterMap = HashMap; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum Compression { + Gzip, + Zstd, +} + +impl FromStr for Compression { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "gzip" => Ok(Compression::Gzip), + "zstd" => Ok(Compression::Zstd), + _ => Err(anyhow::anyhow!("Unknown compression type: {}", s)), + } + } +} + #[derive(Debug, Clone, Parser)] #[clap(author, version, about)] struct Args { @@ -117,6 +130,10 @@ struct Args { #[command(subcommand)] action: Action, + + /// Compression default: NONE, [gzip, zstd] + #[clap(long)] + compression: Option, } impl Args { @@ -135,6 +152,13 @@ impl Args { .tls_config(tls_config)? .max_decoding_message_size(self.max_decoding_message_size); + if let Some(compression) = self.compression { + match compression { + Compression::Gzip => builder = builder.accept_compressed(tonic::codec::CompressionEncoding::Gzip), + Compression::Zstd => builder = builder.accept_compressed(tonic::codec::CompressionEncoding::Zstd), + } + } + if let Some(duration) = self.connect_timeout_ms { builder = builder.connect_timeout(Duration::from_millis(duration)); } From 43e24824173d6e0e267f274a6d4971674da24267 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 15:40:37 -0400 Subject: [PATCH 17/20] metric: grpc_subscriber_queue_size --- examples/rust/src/bin/client.rs | 16 ++++++++++++--- yellowstone-grpc-geyser/src/grpc.rs | 8 ++++++-- yellowstone-grpc-geyser/src/metrics.rs | 15 ++++++++++++++ yellowstone-grpc-geyser/src/util/stream.rs | 24 ++++++++++++++-------- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/examples/rust/src/bin/client.rs b/examples/rust/src/bin/client.rs index f6ddb846..b47f090d 100644 --- a/examples/rust/src/bin/client.rs +++ b/examples/rust/src/bin/client.rs @@ -11,7 +11,13 @@ use { solana_signature::Signature, solana_transaction_status::UiTransactionEncoding, std::{ - collections::HashMap, env, fs::File, path::PathBuf, str::FromStr, sync::Arc, time::{Duration, Instant, SystemTime, UNIX_EPOCH} + collections::HashMap, + env, + fs::File, + path::PathBuf, + str::FromStr, + sync::Arc, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }, tokio::{fs, sync::Mutex}, tonic::transport::{channel::ClientTlsConfig, Certificate}, @@ -154,8 +160,12 @@ impl Args { if let Some(compression) = self.compression { match compression { - Compression::Gzip => builder = builder.accept_compressed(tonic::codec::CompressionEncoding::Gzip), - Compression::Zstd => builder = builder.accept_compressed(tonic::codec::CompressionEncoding::Zstd), + Compression::Gzip => { + builder = builder.accept_compressed(tonic::codec::CompressionEncoding::Gzip) + } + Compression::Zstd => { + builder = builder.accept_compressed(tonic::codec::CompressionEncoding::Zstd) + } } } diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index 443026b2..3b0c30d0 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -2,8 +2,9 @@ use { crate::{ config::{ConfigGrpc, ConfigTokio}, metrics::{ - self, set_subscriber_pace, set_subscriber_recv_bandwidth_load, - set_subscriber_send_bandwidth_load, DebugClientMessage, + self, set_subscriber_pace, set_subscriber_queue_size, + set_subscriber_recv_bandwidth_load, set_subscriber_send_bandwidth_load, + DebugClientMessage, }, util::{ ema::{Ema, EmaReactivity, DEFAULT_EMA_WINDOW}, @@ -938,6 +939,8 @@ impl GrpcService { stream_tx.estimated_consuming_rate().per_second() as i64, ); + set_subscriber_queue_size(&subscriber_id, stream_tx.queue_size()); + tokio::select! { mut message = client_rx.recv() => { // forward to latest filter @@ -1085,6 +1088,7 @@ impl GrpcService { set_subscriber_recv_bandwidth_load(&subscriber_id, 0); set_subscriber_send_bandwidth_load(&subscriber_id, 0); set_subscriber_pace(&subscriber_id, 0); + set_subscriber_queue_size(&subscriber_id, 0); metrics::connections_total_dec(); DebugClientMessage::maybe_send(&debug_client_tx, || DebugClientMessage::Removed { id }); diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index 9e5dc9c7..a5751b3b 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -99,6 +99,14 @@ lazy_static::lazy_static! { &["subscriber_id"] ).unwrap(); + static ref GRPC_SUBSCRIBER_QUEUE_SIZE: IntGaugeVec = IntGaugeVec::new( + Opts::new( + "grpc_subscriber_queue_size", + "Current size of subscriber channel queue" + ), + &["subscriber_id"] + ).unwrap(); + static ref GRPC_SUBCRIBER_RX_LOAD: IntGaugeVec = IntGaugeVec::new( Opts::new( "grpc_subscriber_recv_bandwidth_load", @@ -252,6 +260,7 @@ impl PrometheusService { register!(GEYSER_ACCOUNT_UPDATE_RECEIVED); register!(GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD); register!(GRPC_SUBCRIBER_RX_LOAD); + register!(GRPC_SUBSCRIBER_QUEUE_SIZE); VERSION .with_label_values(&[ @@ -457,3 +466,9 @@ pub fn set_subscriber_recv_bandwidth_load>(subscriber_id: S, load: .with_label_values(&[subscriber_id.as_ref()]) .set(load); } + +pub fn set_subscriber_queue_size>(subscriber_id: S, size: u64) { + GRPC_SUBSCRIBER_QUEUE_SIZE + .with_label_values(&[subscriber_id.as_ref()]) + .set(size as i64); +} diff --git a/yellowstone-grpc-geyser/src/util/stream.rs b/yellowstone-grpc-geyser/src/util/stream.rs index 94ec94ad..0d6bab4c 100644 --- a/yellowstone-grpc-geyser/src/util/stream.rs +++ b/yellowstone-grpc-geyser/src/util/stream.rs @@ -2,7 +2,10 @@ use { crate::util::ema::{Ema, EmaCurrentLoad, EmaReactivity, DEFAULT_EMA_WINDOW}, futures::Stream, std::{ - sync::Arc, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, task::{Context, Poll}, time::{Duration, Instant}, }, @@ -26,6 +29,7 @@ pub trait TrafficWeighted { #[derive(Debug)] struct Shared { + queue_size: AtomicU64, send_ema: Ema, rx_ema: Ema, } @@ -46,12 +50,14 @@ impl Shared { fn add_load(&self, weight: u32, now: Instant) { // Kept parameter type as u32 self.send_ema.record_load(now, weight); // Cast weight to u64 for compatibility + self.queue_size.fetch_add(1, Ordering::Relaxed); } #[inline] fn decr_load(&self, weight: u32, now: Instant) { // Kept parameter type as u32 self.rx_ema.record_load(now, weight); // Cast weight to u64 for compatibility + self.queue_size.fetch_sub(1, Ordering::Relaxed); } } @@ -125,6 +131,7 @@ where stats_settings.rx_ema_window, stats_settings.rx_ema_reactivity, ), + queue_size: AtomicU64::new(0), // Initialize queue size to 0 }); let sender = LoadAwareSender { shared: Arc::clone(&shared), @@ -185,6 +192,10 @@ where self.shared.send_ema.record_no_load(Instant::now()); self.shared.rx_ema.record_no_load(Instant::now()); } + + pub fn queue_size(&self) -> u64 { + self.shared.queue_size.load(Ordering::Relaxed) + } } /// @@ -253,18 +264,12 @@ mod tests { let (sender, mut receiver) = load_aware_channel(10, Default::default()); sender.send(TestItem(5)).await.unwrap(); + assert_eq!(sender.queue_size(), 1); let received = receiver.recv().await.unwrap(); - + assert_eq!(sender.queue_size(), 0); assert_eq!(received.0, 5); } - #[tokio::test] - async fn test_load_tracking() { - let (sender, mut receiver) = load_aware_channel(10, Default::default()); - sender.send(TestItem(5)).await.unwrap(); - receiver.recv().await.unwrap(); - } - #[tokio::test] async fn test_stream_behavior() { let (sender, receiver) = load_aware_channel(10, Default::default()); @@ -273,6 +278,7 @@ mod tests { sender.send(TestItem(2)).await.unwrap(); sender.send(TestItem(3)).await.unwrap(); + assert_eq!(sender.queue_size(), 3); drop(sender); let mut stream = receiver; From 44f5744f8006bbfa8777a73ae71114211990f1ec Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Fri, 1 Aug 2025 15:48:01 -0400 Subject: [PATCH 18/20] geyser: new slot metrics --- yellowstone-grpc-geyser/src/grpc.rs | 10 ++++++++++ yellowstone-grpc-geyser/src/metrics.rs | 27 +++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index 3b0c30d0..e35a794a 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -572,6 +572,16 @@ impl GrpcService { // Update metrics if let Message::Slot(slot_message) = &message { metrics::update_slot_plugin_status(slot_message.status, slot_message.slot); + match slot_message.status { + SlotStatus::FirstShredReceived => { + metrics::incr_slot_first_shred_received_counter(); + } + SlotStatus::Completed => { + // Completed is similar to slow fully downloaded by agave. + metrics::incr_slot_completed_counter(); + } + _ => {} + } } // Update blocks info diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index a5751b3b..42a3d8fb 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -13,7 +13,8 @@ use { }, log::{error, info}, prometheus::{ - Histogram, HistogramOpts, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, TextEncoder, + Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, + TextEncoder, }, solana_clock::Slot, std::{ @@ -47,6 +48,20 @@ lazy_static::lazy_static! { &["status"] ).unwrap(); + /// + /// First shred received from agave tells us at which rate this rpc receives the latest slot. + /// + static ref SLOT_FIRST_SHRED_RECEIVED_COUNTER: IntCounter = IntCounter::new( + "slot_first_shred_received_counter", "Counter of first shred received from agave", + ).unwrap(); + + /// + /// Slot completed is equivalent to fully downloaded by agave, using `rate()` to calculate the rate of downlaoded slots. + /// + static ref SLOT_COMPLETED_COUNTER: IntCounter = IntCounter::new( + "slot_completed_counter", "Counter of completed slots", + ).unwrap(); + static ref INVALID_FULL_BLOCKS: IntGaugeVec = IntGaugeVec::new( Opts::new("invalid_full_blocks_total", "Total number of fails on constructin full blocks"), &["reason"] @@ -261,6 +276,8 @@ impl PrometheusService { register!(GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD); register!(GRPC_SUBCRIBER_RX_LOAD); register!(GRPC_SUBSCRIBER_QUEUE_SIZE); + register!(SLOT_FIRST_SHRED_RECEIVED_COUNTER); + register!(SLOT_COMPLETED_COUNTER); VERSION .with_label_values(&[ @@ -472,3 +489,11 @@ pub fn set_subscriber_queue_size>(subscriber_id: S, size: u64) { .with_label_values(&[subscriber_id.as_ref()]) .set(size as i64); } + +pub fn incr_slot_first_shred_received_counter() { + SLOT_FIRST_SHRED_RECEIVED_COUNTER.inc(); +} + +pub fn incr_slot_completed_counter() { + SLOT_COMPLETED_COUNTER.inc(); +} From a5e380f8deddf45c08e3cf4088c2e5ec2f71e7a5 Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Mon, 4 Aug 2025 08:23:37 -0400 Subject: [PATCH 19/20] metrics: removed redundant metrics --- yellowstone-grpc-geyser/src/grpc.rs | 10 ---------- yellowstone-grpc-geyser/src/metrics.rs | 27 +------------------------- 2 files changed, 1 insertion(+), 36 deletions(-) diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index e35a794a..3b0c30d0 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -572,16 +572,6 @@ impl GrpcService { // Update metrics if let Message::Slot(slot_message) = &message { metrics::update_slot_plugin_status(slot_message.status, slot_message.slot); - match slot_message.status { - SlotStatus::FirstShredReceived => { - metrics::incr_slot_first_shred_received_counter(); - } - SlotStatus::Completed => { - // Completed is similar to slow fully downloaded by agave. - metrics::incr_slot_completed_counter(); - } - _ => {} - } } // Update blocks info diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index 42a3d8fb..a5751b3b 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -13,8 +13,7 @@ use { }, log::{error, info}, prometheus::{ - Histogram, HistogramOpts, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, - TextEncoder, + Histogram, HistogramOpts, IntCounterVec, IntGauge, IntGaugeVec, Opts, Registry, TextEncoder, }, solana_clock::Slot, std::{ @@ -48,20 +47,6 @@ lazy_static::lazy_static! { &["status"] ).unwrap(); - /// - /// First shred received from agave tells us at which rate this rpc receives the latest slot. - /// - static ref SLOT_FIRST_SHRED_RECEIVED_COUNTER: IntCounter = IntCounter::new( - "slot_first_shred_received_counter", "Counter of first shred received from agave", - ).unwrap(); - - /// - /// Slot completed is equivalent to fully downloaded by agave, using `rate()` to calculate the rate of downlaoded slots. - /// - static ref SLOT_COMPLETED_COUNTER: IntCounter = IntCounter::new( - "slot_completed_counter", "Counter of completed slots", - ).unwrap(); - static ref INVALID_FULL_BLOCKS: IntGaugeVec = IntGaugeVec::new( Opts::new("invalid_full_blocks_total", "Total number of fails on constructin full blocks"), &["reason"] @@ -276,8 +261,6 @@ impl PrometheusService { register!(GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD); register!(GRPC_SUBCRIBER_RX_LOAD); register!(GRPC_SUBSCRIBER_QUEUE_SIZE); - register!(SLOT_FIRST_SHRED_RECEIVED_COUNTER); - register!(SLOT_COMPLETED_COUNTER); VERSION .with_label_values(&[ @@ -489,11 +472,3 @@ pub fn set_subscriber_queue_size>(subscriber_id: S, size: u64) { .with_label_values(&[subscriber_id.as_ref()]) .set(size as i64); } - -pub fn incr_slot_first_shred_received_counter() { - SLOT_FIRST_SHRED_RECEIVED_COUNTER.inc(); -} - -pub fn incr_slot_completed_counter() { - SLOT_COMPLETED_COUNTER.inc(); -} From ee4d34812b5d68af3228be7d5c262ac01b74c3ad Mon Sep 17 00:00:00 2001 From: Louis-Vincent Date: Mon, 4 Aug 2025 08:29:56 -0400 Subject: [PATCH 20/20] remove redundant metrics --- yellowstone-grpc-geyser/src/grpc.rs | 21 +++------------------ yellowstone-grpc-geyser/src/metrics.rs | 15 --------------- yellowstone-grpc-geyser/src/util/stream.rs | 19 +++++++++++-------- 3 files changed, 14 insertions(+), 41 deletions(-) diff --git a/yellowstone-grpc-geyser/src/grpc.rs b/yellowstone-grpc-geyser/src/grpc.rs index 3b0c30d0..5b778a7b 100644 --- a/yellowstone-grpc-geyser/src/grpc.rs +++ b/yellowstone-grpc-geyser/src/grpc.rs @@ -2,12 +2,11 @@ use { crate::{ config::{ConfigGrpc, ConfigTokio}, metrics::{ - self, set_subscriber_pace, set_subscriber_queue_size, - set_subscriber_recv_bandwidth_load, set_subscriber_send_bandwidth_load, - DebugClientMessage, + self, set_subscriber_queue_size, set_subscriber_recv_bandwidth_load, + set_subscriber_send_bandwidth_load, DebugClientMessage, }, util::{ - ema::{Ema, EmaReactivity, DEFAULT_EMA_WINDOW}, + ema::{EmaReactivity, DEFAULT_EMA_WINDOW}, stream::{ load_aware_channel, LoadAwareReceiver, LoadAwareSender, StatsSettings, TrafficWeighted, @@ -916,19 +915,8 @@ impl GrpcService { ) .await; } - - let client_loop_pace = Ema::builder() - .window(DEFAULT_EMA_WINDOW) - .reactivity(EmaReactivity::Reactive) - .build(); - if is_alive { 'outer: loop { - set_subscriber_pace( - &subscriber_id, - client_loop_pace.current_load().per_second() as i64, - ); - set_subscriber_send_bandwidth_load( &subscriber_id, stream_tx.estimated_send_rate().per_second() as i64, @@ -1045,8 +1033,6 @@ impl GrpcService { } }; - client_loop_pace.record_load(Instant::now().into(), messages.len() as u32); - if commitment == filter.get_commitment_level() { for (_msgid, message) in messages.iter() { for message in filter.get_updates(message, Some(commitment)) { @@ -1087,7 +1073,6 @@ impl GrpcService { } set_subscriber_recv_bandwidth_load(&subscriber_id, 0); set_subscriber_send_bandwidth_load(&subscriber_id, 0); - set_subscriber_pace(&subscriber_id, 0); set_subscriber_queue_size(&subscriber_id, 0); metrics::connections_total_dec(); diff --git a/yellowstone-grpc-geyser/src/metrics.rs b/yellowstone-grpc-geyser/src/metrics.rs index a5751b3b..85e9cd95 100644 --- a/yellowstone-grpc-geyser/src/metrics.rs +++ b/yellowstone-grpc-geyser/src/metrics.rs @@ -83,14 +83,6 @@ lazy_static::lazy_static! { &["subscriber_id"] ).unwrap(); - static ref GRPC_SUBSCRIBER_MESSAGE_PROCESSING_PACE: IntGaugeVec = IntGaugeVec::new( - Opts::new( - "grpc_subscriber_message_processing_pace_sec", - "How many subscriber loop process incoming geyser message per second" - ), - &["subscriber_id"] - ).unwrap(); - static ref GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD: IntGaugeVec = IntGaugeVec::new( Opts::new( "grpc_subscriber_send_bandwidth_load", @@ -256,7 +248,6 @@ impl PrometheusService { register!(MISSED_STATUS_MESSAGE); register!(GRPC_MESSAGE_SENT); register!(GRPC_BYTES_SENT); - register!(GRPC_SUBSCRIBER_MESSAGE_PROCESSING_PACE); register!(GEYSER_ACCOUNT_UPDATE_RECEIVED); register!(GRPC_SUBSCRIBER_SEND_BANDWIDTH_LOAD); register!(GRPC_SUBCRIBER_RX_LOAD); @@ -445,12 +436,6 @@ pub fn missed_status_message_inc(status: SlotStatus) { .inc() } -pub fn set_subscriber_pace>(subscriber_id: S, pace: i64) { - GRPC_SUBSCRIBER_MESSAGE_PROCESSING_PACE - .with_label_values(&[subscriber_id.as_ref()]) - .set(pace); -} - pub fn observe_geyser_account_update_received(data_bytesize: usize) { GEYSER_ACCOUNT_UPDATE_RECEIVED.observe(data_bytesize as f64 / 1024.0); } diff --git a/yellowstone-grpc-geyser/src/util/stream.rs b/yellowstone-grpc-geyser/src/util/stream.rs index 0d6bab4c..8071574b 100644 --- a/yellowstone-grpc-geyser/src/util/stream.rs +++ b/yellowstone-grpc-geyser/src/util/stream.rs @@ -122,15 +122,18 @@ where { let (inner_sender, inner_receiver) = tokio::sync::mpsc::channel(capacity); + let send_ema = Ema::builder() + .window(stats_settings.tx_ema_window) + .reactivity(stats_settings.tx_ema_reactivity) + .build(); + let rx_ema = Ema::builder() + .window(stats_settings.rx_ema_window) + .reactivity(stats_settings.rx_ema_reactivity) + .build(); + let shared = Arc::new(Shared { - send_ema: Ema::new( - stats_settings.tx_ema_window, - stats_settings.tx_ema_reactivity, - ), - rx_ema: Ema::new( - stats_settings.rx_ema_window, - stats_settings.rx_ema_reactivity, - ), + send_ema, + rx_ema, queue_size: AtomicU64::new(0), // Initialize queue size to 0 }); let sender = LoadAwareSender {