Skip to content

Commit 2710bae

Browse files
committed
feat(metrics): add gossip arrival-time histograms and position counters
Network health had plenty of block-production timing but nothing about reception timing, so "are votes arriving late, or not arriving at all?" could only be answered out-of-band with tooling/event-monitor's collector-side clock. Adds three histograms recording the absolute distance between a gossip message's arrival and the start of the interval it was due in, plus three counters splitting arrivals into before/inside/after that interval. The interval is what counts as inside, not the slot: an attestation landing in its own slot's aggregation interval missed the production interval it was due in, so it reads as after. Aggregates anchor to the most recent aggregation-interval boundary instead of their own data.slot, since a stale-group catch-up aggregate legitimately carries an older slot and would otherwise fill the histogram with large values that are not a health problem. That bounds their delay to one slot and makes before unreachable, so the aggregate counter does not export that series. Blocks are sampled only when received on gossip; req/resp sync backfill delivers them many slots after they were due and would swamp the histogram. Threading a BlockSource through new_block to tell the two apart also fixes ChainEvent::BlockGossip, which events.rs already documents as gossip-only yet until now also fired for sync-fetched blocks.
1 parent 32700c8 commit 2710bae

6 files changed

Lines changed: 406 additions & 9 deletions

File tree

crates/blockchain/src/lib.rs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet, VecDeque};
22
use std::time::{Duration, Instant, SystemTime};
33

44
use ethlambda_crypto::signature::{ValidatorPublicKey, ValidatorSignature};
5-
use ethlambda_network_api::{BlockChainToP2PRef, InitP2P};
5+
use ethlambda_network_api::{BlockChainToP2PRef, BlockSource, InitP2P};
66
use ethlambda_state_transition::is_proposer;
77
use ethlambda_storage::{ALL_TABLES, Store};
88
use ethlambda_types::{
@@ -109,6 +109,20 @@ impl SlotInterval {
109109
_ => unreachable!("slots only have 5 intervals"),
110110
}
111111
}
112+
113+
/// Milliseconds from genesis to the start of this interval in `slot`.
114+
///
115+
/// Inverse of [`Self::from_ms_since_genesis`].
116+
pub(crate) fn to_ms_since_genesis(self, slot: u64) -> u64 {
117+
let interval = match self {
118+
Self::BlockPublication => 0,
119+
Self::AttestationProduction => 1,
120+
Self::Aggregation => 2,
121+
Self::SafeTargetUpdate => 3,
122+
Self::EndOfSlot => 4,
123+
};
124+
slot * MILLISECONDS_PER_SLOT + interval * MILLISECONDS_PER_INTERVAL
125+
}
112126
}
113127

114128
/// Milliseconds until the next interval boundary, measured relative to genesis.
@@ -1367,16 +1381,37 @@ impl Handler<InitP2P> for BlockChainServer {
13671381

13681382
impl Handler<NewBlock> for BlockChainServer {
13691383
async fn handle(&mut self, msg: NewBlock, _ctx: &Context<Self>) {
1370-
self.events.emit(ChainEvent::BlockGossip {
1371-
slot: msg.block.message.slot,
1372-
block: msg.block.message.hash_tree_root(),
1373-
});
1384+
let arrival_ms = unix_now_ms();
1385+
// Gate both the event and the arrival metric on BlockSource::Gossip for
1386+
// two reasons: `ChainEvent::BlockGossip` is documented (events.rs) as "a
1387+
// block seen on gossip, before import", yet without this gate it also
1388+
// fired for req/resp sync blocks; and sync backfill delivers blocks many
1389+
// slots after they were due, which would swamp the arrival histogram
1390+
// with stale deltas that reflect catch-up speed, not gossip timeliness.
1391+
// `self.on_block(msg.block)` still runs for every source below: it is
1392+
// the import path and must not be gated.
1393+
if msg.source == BlockSource::Gossip {
1394+
let slot = msg.block.message.slot;
1395+
self.events.emit(ChainEvent::BlockGossip {
1396+
slot,
1397+
block: msg.block.message.hash_tree_root(),
1398+
});
1399+
let genesis_ms = self.store.config().expect("config exists").genesis_time * 1000;
1400+
metrics::observe_gossip_block_arrival(arrival_ms, genesis_ms, slot);
1401+
}
13741402
self.on_block(msg.block);
13751403
}
13761404
}
13771405

13781406
impl Handler<NewAttestation> for BlockChainServer {
13791407
async fn handle(&mut self, msg: NewAttestation, ctx: &Context<Self>) {
1408+
let arrival_ms = unix_now_ms();
1409+
let genesis_ms = self.store.config().expect("config exists").genesis_time * 1000;
1410+
metrics::observe_gossip_attestation_arrival(
1411+
arrival_ms,
1412+
genesis_ms,
1413+
msg.attestation.data.slot,
1414+
);
13801415
self.on_gossip_attestation(&msg.attestation);
13811416
// Early aggregation only advances the current slot's group counts, so a
13821417
// late- or future-slot attestation can never cross the threshold; skip
@@ -1390,6 +1425,9 @@ impl Handler<NewAttestation> for BlockChainServer {
13901425

13911426
impl Handler<NewAggregatedAttestation> for BlockChainServer {
13921427
async fn handle(&mut self, msg: NewAggregatedAttestation, _ctx: &Context<Self>) {
1428+
let arrival_ms = unix_now_ms();
1429+
let genesis_ms = self.store.config().expect("config exists").genesis_time * 1000;
1430+
metrics::observe_gossip_aggregation_arrival(arrival_ms, genesis_ms);
13931431
self.on_gossip_aggregated_attestation(msg.attestation);
13941432
}
13951433
}
@@ -1478,3 +1516,23 @@ impl Handler<AggregationDeadline> for BlockChainServer {
14781516
}
14791517
}
14801518
}
1519+
1520+
#[cfg(test)]
1521+
mod tests {
1522+
use super::*;
1523+
1524+
#[test]
1525+
fn interval_ms_round_trips() {
1526+
let intervals = [
1527+
SlotInterval::BlockPublication,
1528+
SlotInterval::AttestationProduction,
1529+
SlotInterval::Aggregation,
1530+
SlotInterval::SafeTargetUpdate,
1531+
SlotInterval::EndOfSlot,
1532+
];
1533+
for interval in intervals {
1534+
let ms = interval.to_ms_since_genesis(7);
1535+
assert_eq!(SlotInterval::from_ms_since_genesis(ms), interval);
1536+
}
1537+
}
1538+
}

0 commit comments

Comments
 (0)