use std::collections::{HashMap, VecDeque};
use crate::tui::api::ClusterStatus;
use crate::tui::prom;
pub const MAX_RETENTION_SECS: u64 = 6 * 3600;
pub const MIN_INTERVAL_SECS: u64 = 1;
pub const DEFAULT_RETENTION_SECS: u64 = 3600;
pub const DEFAULT_INTERVAL_SECS: u64 = 5;
pub fn validate(retention_secs: u64, interval_secs: u64) -> Result<(), String> {
if interval_secs < MIN_INTERVAL_SECS {
return Err("history.interval: use 1s or more".to_owned());
}
if retention_secs > MAX_RETENTION_SECS {
return Err("history.retention: use 6h or less".to_owned());
}
if retention_secs < interval_secs {
return Err("history.retention: it must hold at least one sample".to_owned());
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Series {
MemoryUsed,
MemoryLimit,
MemoryMessages,
DiskMessages,
ActiveRequests,
RateLimited,
ChannelRtt,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub at: u64,
pub value: f64,
pub partial: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Ring {
cap: usize,
points: VecDeque<Point>,
}
impl Ring {
pub fn new(cap: usize) -> Ring {
Ring {
cap: cap.max(1),
points: VecDeque::new(),
}
}
pub fn push(&mut self, point: Point) {
if self.points.len() == self.cap {
self.points.pop_front();
}
self.points.push_back(point);
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn last(&self) -> Option<Point> {
self.points.back().copied()
}
pub fn window(&self, now: u64, window_ms: u64) -> Vec<Point> {
let from = now.saturating_sub(window_ms);
self.points
.iter()
.copied()
.filter(|p| p.at >= from && p.at <= now)
.collect()
}
pub fn collected_ms(&self, now: u64) -> u64 {
self.points.front().map_or(0, |p| now.saturating_sub(p.at))
}
}
pub fn summary(points: &[Point]) -> Option<(f64, f64, f64)> {
let last = points.last()?.value;
let min = points.iter().map(|p| p.value).fold(f64::INFINITY, f64::min);
let max = points
.iter()
.map(|p| p.value)
.fold(f64::NEG_INFINITY, f64::max);
Some((min, max, last))
}
#[derive(Debug, Clone)]
pub struct History {
cap: usize,
interval_ms: u64,
rings: HashMap<(String, Series), Ring>,
counters: HashMap<String, (u64, f64)>,
last_sample: Option<u64>,
}
impl History {
pub fn new(retention_secs: u64, interval_secs: u64) -> History {
let interval = interval_secs.max(MIN_INTERVAL_SECS);
History {
cap: (retention_secs / interval).max(1) as usize,
interval_ms: interval * 1000,
rings: HashMap::new(),
counters: HashMap::new(),
last_sample: None,
}
}
pub fn capacity(&self) -> usize {
self.cap
}
pub fn interval_ms(&self) -> u64 {
self.interval_ms
}
pub fn retention_ms(&self) -> u64 {
self.cap as u64 * self.interval_ms
}
pub fn clear(&mut self) {
self.rings.clear();
self.counters.clear();
self.last_sample = None;
}
pub fn series(&self, node: &str, series: Series) -> Option<&Ring> {
self.rings.get(&(node.to_owned(), series))
}
fn push(&mut self, node: &str, series: Series, point: Point) {
let cap = self.cap;
self.rings
.entry((node.to_owned(), series))
.or_insert_with(|| Ring::new(cap))
.push(point);
}
pub fn sample(&mut self, now: u64, status: &ClusterStatus) -> bool {
if self.last_sample.is_some_and(|t| now < t + self.interval_ms) {
return false;
}
self.last_sample = Some(now);
for node in status.nodes.iter().filter(|n| n.reachable) {
let mk = |value: f64| Point {
at: now,
value,
partial: status.partial,
};
let id = node.node_id.as_str();
self.push(id, Series::MemoryUsed, mk(node.memory_used as f64));
self.push(id, Series::MemoryLimit, mk(node.memory_limit as f64));
self.push(id, Series::MemoryMessages, mk(node.memory_messages as f64));
self.push(id, Series::DiskMessages, mk(node.disk_messages as f64));
self.push(id, Series::ActiveRequests, mk(node.active_requests));
if let Some(rtt) = node.rtt_p95 {
self.push(id, Series::ChannelRtt, mk(rtt));
}
let previous = self
.counters
.insert(id.to_owned(), (now, node.rate_limited));
if let Some((then, was)) = previous {
let seconds = (now - then) as f64 / 1000.0;
if let Some(per_second) = prom::rate(was, node.rate_limited, seconds) {
self.push(id, Series::RateLimited, mk(per_second));
}
}
}
true
}
}
#[cfg(test)]
mod tests;