use std::time::Duration;
use anyhow::Result;
use linera_base::identifiers::ChainId;
use linera_client::client_context::ClientContext;
use linera_core::{
data_types::ChainInfoQuery,
node::{ValidatorNode, ValidatorNodeProvider as _},
};
use tokio::time::{sleep, Instant};
use super::{
progress::Progress,
report::{PerChainTipLag, TipLagReport, TipLagSample, TipLagTrend},
rpc::timed,
};
const STABLE_BAND: i64 = 2;
pub async fn run<N, Env>(
node: &N,
context: &ClientContext<Env>,
chains: &[ChainId],
samples: usize,
interval: Duration,
rpc_timeout: Duration,
progress: &Progress,
) -> Result<TipLagReport>
where
N: ValidatorNode,
Env: linera_core::Environment,
{
let started = Instant::now();
let phase = progress.phase("L6 tip-lag", Some(samples.max(1) as u64));
let committee_nodes = committee_nodes(context);
let mut per_chain: Vec<PerChainTipLag> = chains
.iter()
.map(|c| PerChainTipLag {
chain_id: c.to_string(),
samples: Vec::with_capacity(samples),
trend: TipLagTrend::Stable,
})
.collect();
for i in 0..samples.max(1) {
if i > 0 {
let mut remaining = interval.as_secs();
while remaining > 0 {
phase.set_message(format!("next sample in {remaining}s"));
sleep(Duration::from_secs(1)).await;
remaining -= 1;
}
}
phase.set_message(format!("sample {}/{}", i + 1, samples.max(1)));
let t_secs = started.elapsed().as_secs();
for (idx, &chain) in chains.iter().enumerate() {
let candidate_tip = timed(
rpc_timeout,
node.handle_chain_info_query(ChainInfoQuery::new(chain)),
)
.await
.map_or(0, |response| response.info.next_block_height.0);
let mut reference_tip = 0u64;
for committee_node in &committee_nodes {
if let Ok(response) = timed(
rpc_timeout,
committee_node.handle_chain_info_query(ChainInfoQuery::new(chain)),
)
.await
{
reference_tip = reference_tip.max(response.info.next_block_height.0);
}
}
let reference = i64::try_from(reference_tip).unwrap_or(i64::MAX);
let candidate = i64::try_from(candidate_tip).unwrap_or(i64::MAX);
per_chain[idx].samples.push(TipLagSample {
t_secs,
candidate_tip,
reference_tip,
lag_blocks: reference.saturating_sub(candidate),
});
}
phase.inc(1);
}
phase.finish_ok();
for pc in &mut per_chain {
pc.trend = compute_trend(&pc.samples);
}
Ok(TipLagReport { per_chain })
}
fn committee_nodes<Env>(context: &ClientContext<Env>) -> Vec<linera_rpc::Client>
where
Env: linera_core::Environment,
{
let provider = context.make_node_provider();
context
.genesis_config
.committee
.validators()
.values()
.filter_map(|state| provider.make_node(&state.network_address).ok())
.collect()
}
pub(super) fn compute_trend(samples: &[TipLagSample]) -> TipLagTrend {
let (Some(first), Some(last)) = (samples.first(), samples.last()) else {
return TipLagTrend::Stable;
};
if samples.len() < 2 {
return TipLagTrend::Stable;
}
let delta = last.lag_blocks - first.lag_blocks;
if delta.abs() <= STABLE_BAND {
TipLagTrend::Stable
} else if delta < 0 {
TipLagTrend::Converging
} else {
TipLagTrend::Diverging
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(t: u64, lag: i64) -> TipLagSample {
TipLagSample {
t_secs: t,
candidate_tip: 0,
reference_tip: u64::try_from(lag).unwrap_or(0),
lag_blocks: lag,
}
}
#[test]
fn stable_within_band() {
assert_eq!(
compute_trend(&[sample(0, 10), sample(60, 11)]),
TipLagTrend::Stable
);
}
#[test]
fn converging_when_lag_shrinks() {
assert_eq!(
compute_trend(&[sample(0, 50), sample(60, 10)]),
TipLagTrend::Converging
);
}
#[test]
fn diverging_when_lag_grows() {
assert_eq!(
compute_trend(&[sample(0, 5), sample(60, 80)]),
TipLagTrend::Diverging
);
}
#[test]
fn single_sample_is_stable() {
assert_eq!(compute_trend(&[sample(0, 10)]), TipLagTrend::Stable);
}
#[test]
fn empty_is_stable() {
assert_eq!(compute_trend(&[]), TipLagTrend::Stable);
}
}