#![allow(clippy::float_arithmetic)]
use std::{
fmt,
net::SocketAddr,
pin::Pin,
sync::{
atomic::{AtomicU64, Ordering},
Arc, Weak,
},
task::{ready, Context, Poll},
};
use anyhow::Context as _;
use vise::{
Collector, Counter, EncodeLabelSet, EncodeLabelValue, Family, Gauge, GaugeGuard, Metrics, Unit,
};
use zksync_concurrency::{ctx, io, net, time::Instant};
use crate::Network;
#[pin_project::pin_project]
pub(crate) struct MeteredStream {
#[pin]
stream: net::tcp::Stream,
stats: Arc<MeteredStreamStats>,
_active: GaugeGuard,
}
impl MeteredStream {
pub(crate) async fn connect(ctx: &ctx::Ctx, addr: SocketAddr) -> ctx::Result<Self> {
let stream = net::tcp::connect(ctx, addr).await?.context("connect()")?;
Ok(Self::new(stream, Direction::Outbound)?)
}
pub(crate) async fn accept(
ctx: &ctx::Ctx,
listener: &mut net::tcp::Listener,
) -> ctx::Result<Self> {
let stream = net::tcp::accept(ctx, listener).await?.context("accept()")?;
Ok(Self::new(stream, Direction::Inbound)?)
}
#[cfg(test)]
pub(crate) async fn test_pipe(ctx: &ctx::Ctx) -> (Self, Self) {
let (outbound_stream, inbound_stream) = net::tcp::testonly::pipe(ctx).await;
let outbound_stream = Self::new(outbound_stream, Direction::Outbound).unwrap();
let inbound_stream = Self::new(inbound_stream, Direction::Inbound).unwrap();
(outbound_stream, inbound_stream)
}
fn new(stream: net::tcp::Stream, direction: Direction) -> anyhow::Result<Self> {
TCP_METRICS.established[&direction].inc();
let addr = stream.peer_addr().context("peer_addr()")?;
Ok(Self {
stream,
stats: Arc::new(MeteredStreamStats::new(addr)),
_active: TCP_METRICS.active[&direction].inc_guard(1),
})
}
pub(crate) fn stats(&self) -> Arc<MeteredStreamStats> {
self.stats.clone()
}
}
impl std::ops::Deref for MeteredStream {
type Target = net::tcp::Stream;
fn deref(&self) -> &Self::Target {
&self.stream
}
}
impl io::AsyncRead for MeteredStream {
#[inline(always)]
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut io::ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.project();
let before = buf.remaining();
let res = this.stream.poll_read(cx, buf);
let amount = (before - buf.remaining()) as u64;
TCP_METRICS.received.inc_by(amount);
this.stats.read(amount);
res
}
}
impl io::AsyncWrite for MeteredStream {
#[inline(always)]
fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<io::Result<usize>> {
let this = self.project();
let res = ready!(this.stream.poll_write(cx, buf))?;
TCP_METRICS.sent.inc_by(res as u64);
this.stats.wrote(res as u64);
Poll::Ready(Ok(res))
}
#[inline(always)]
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
self.project().stream.poll_flush(cx)
}
#[inline(always)]
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll<io::Result<()>> {
self.project().stream.poll_shutdown(cx)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EncodeLabelSet, EncodeLabelValue)]
#[metrics(label = "direction", rename_all = "snake_case")]
enum Direction {
Inbound,
Outbound,
}
#[derive(Debug, Metrics)]
#[metrics(prefix = "network_tcp")]
struct TcpMetrics {
#[metrics(unit = Unit::Bytes)]
sent: Counter,
#[metrics(unit = Unit::Bytes)]
received: Counter,
established: Family<Direction, Counter>,
active: Family<Direction, Gauge>,
}
#[vise::register]
static TCP_METRICS: vise::Global<TcpMetrics> = vise::Global::new();
#[derive(Clone, Debug, PartialEq, Eq, Hash, EncodeLabelSet, EncodeLabelValue)]
#[metrics(label = "build_version")]
pub(crate) struct BuildVersion(String);
impl fmt::Display for BuildVersion {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(fmt)
}
}
#[derive(Debug, Metrics)]
#[metrics(prefix = "network")]
pub(crate) struct NetworkGauges {
gossip_inbound_connections: Gauge<usize>,
gossip_outbound_connections: Gauge<usize>,
consensus_inbound_connections: Gauge<usize>,
consensus_outbound_connections: Gauge<usize>,
gossip_peers_by_build_version: Family<BuildVersion, Gauge<usize>>,
}
impl NetworkGauges {
pub(crate) fn register(state_ref: Weak<Network>) {
#[vise::register]
static COLLECTOR: Collector<Option<NetworkGauges>> = Collector::new();
let register_result = COLLECTOR.before_scrape(move || {
state_ref.upgrade().map(|state| {
let gauges = NetworkGauges::default();
let inbound = state.gossip.inbound.current();
gauges.gossip_inbound_connections.set(inbound.len());
for conn in inbound.values() {
let v = BuildVersion(
conn.build_version
.as_ref()
.map(|v| v.to_string())
.unwrap_or_default(),
);
gauges.gossip_peers_by_build_version[&v].inc_by(1);
}
let outbound = state.gossip.outbound.current();
gauges.gossip_outbound_connections.set(outbound.len());
for conn in outbound.values() {
let v = BuildVersion(
conn.build_version
.as_ref()
.map(|v| v.to_string())
.unwrap_or_default(),
);
gauges.gossip_peers_by_build_version[&v].inc_by(1);
}
if let Some(consensus_state) = &state.consensus {
let len = consensus_state.inbound.current().len();
gauges.consensus_inbound_connections.set(len);
let len = consensus_state.outbound.current().len();
gauges.consensus_outbound_connections.set(len);
}
gauges
})
});
if register_result.is_err() {
tracing::debug!("Failed registering network metrics collector: already registered");
}
}
}
#[derive(Debug)]
pub struct MeteredStreamStats {
pub sent: AtomicU64,
pub received: AtomicU64,
pub established: Instant,
pub peer_addr: SocketAddr,
pub current_minute_sent: AtomicU64,
pub previous_minute_sent: AtomicU64,
pub current_minute_received: AtomicU64,
pub previous_minute_received: AtomicU64,
pub minutes_elapsed_last: AtomicU64,
}
impl MeteredStreamStats {
fn new(peer_addr: SocketAddr) -> Self {
Self {
sent: 0.into(),
received: 0.into(),
established: Instant::now(),
peer_addr,
current_minute_sent: 0.into(),
previous_minute_sent: 0.into(),
current_minute_received: 0.into(),
previous_minute_received: 0.into(),
minutes_elapsed_last: 0.into(),
}
}
fn read(&self, amount: u64) {
self.update_minute();
self.received.fetch_add(amount, Ordering::Relaxed);
self.current_minute_received
.fetch_add(amount, Ordering::Relaxed);
}
fn wrote(&self, amount: u64) {
self.update_minute();
self.sent.fetch_add(amount, Ordering::Relaxed);
self.current_minute_sent
.fetch_add(amount, Ordering::Relaxed);
}
fn update_minute(&self) {
let elapsed_minutes_now = self.established.elapsed().whole_seconds() as u64 / 60;
let elapsed_minutes_last = self.minutes_elapsed_last.load(Ordering::Relaxed);
if elapsed_minutes_now > elapsed_minutes_last {
if elapsed_minutes_now - elapsed_minutes_last > 1 {
self.previous_minute_sent.store(0, Ordering::Relaxed);
self.previous_minute_received.store(0, Ordering::Relaxed);
} else {
self.previous_minute_sent.store(
self.current_minute_sent.load(Ordering::Relaxed),
Ordering::Relaxed,
);
self.previous_minute_received.store(
self.current_minute_received.load(Ordering::Relaxed),
Ordering::Relaxed,
);
}
self.current_minute_sent.store(0, Ordering::Relaxed);
self.current_minute_received.store(0, Ordering::Relaxed);
self.minutes_elapsed_last
.store(elapsed_minutes_now, Ordering::Relaxed);
}
}
pub fn sent_throughput(&self) -> f64 {
let elapsed_minutes_now = self.established.elapsed().whole_seconds() as u64 / 60;
let elapsed_minutes_last = self.minutes_elapsed_last.load(Ordering::Relaxed);
if elapsed_minutes_now - elapsed_minutes_last == 0 {
self.previous_minute_sent.load(Ordering::Relaxed) as f64 / 60.0
} else if elapsed_minutes_now - elapsed_minutes_last == 1 {
self.current_minute_sent.load(Ordering::Relaxed) as f64 / 60.0
} else {
0.0
}
}
pub fn received_throughput(&self) -> f64 {
let elapsed_minutes_now = self.established.elapsed().whole_seconds() as u64 / 60;
let elapsed_minutes_last = self.minutes_elapsed_last.load(Ordering::Relaxed);
if elapsed_minutes_now - elapsed_minutes_last == 0 {
self.previous_minute_received.load(Ordering::Relaxed) as f64 / 60.0
} else if elapsed_minutes_now - elapsed_minutes_last == 1 {
self.current_minute_received.load(Ordering::Relaxed) as f64 / 60.0
} else {
0.0
}
}
}