use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LinkState {
Unknown,
Up,
Down,
}
#[derive(Debug)]
struct LinkTracker {
state: LinkState,
}
#[derive(Debug)]
pub struct ReplicaLinkMetrics {
reconnects_total: AtomicU64,
tracker: Mutex<LinkTracker>,
}
impl Default for ReplicaLinkMetrics {
fn default() -> Self {
Self {
reconnects_total: AtomicU64::new(0),
tracker: Mutex::new(LinkTracker {
state: LinkState::Unknown,
}),
}
}
}
impl ReplicaLinkMetrics {
pub fn observe_state(&self, state: &str) {
let mut tracker = match self.tracker.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
};
match state {
"healthy" => {
if tracker.state == LinkState::Down {
self.reconnects_total.fetch_add(1, Ordering::Relaxed);
}
tracker.state = LinkState::Up;
}
"connecting" if tracker.state == LinkState::Up => {
tracker.state = LinkState::Down;
}
_ => {}
}
}
pub fn reconnects_total(&self) -> u64 {
self.reconnects_total.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_connect_is_not_a_reconnect() {
let metrics = ReplicaLinkMetrics::default();
metrics.observe_state("connecting");
metrics.observe_state("healthy");
assert_eq!(metrics.reconnects_total(), 0);
}
#[test]
fn drop_and_restore_counts_one_reconnect() {
let metrics = ReplicaLinkMetrics::default();
metrics.observe_state("connecting");
metrics.observe_state("healthy");
metrics.observe_state("connecting");
metrics.observe_state("healthy");
assert_eq!(metrics.reconnects_total(), 1);
}
#[test]
fn repeated_connecting_during_one_outage_counts_once() {
let metrics = ReplicaLinkMetrics::default();
metrics.observe_state("healthy");
metrics.observe_state("connecting");
metrics.observe_state("connecting");
metrics.observe_state("connecting");
metrics.observe_state("healthy");
assert_eq!(metrics.reconnects_total(), 1);
}
#[test]
fn multiple_outages_accumulate() {
let metrics = ReplicaLinkMetrics::default();
metrics.observe_state("healthy");
for _ in 0..5 {
metrics.observe_state("connecting");
metrics.observe_state("healthy");
}
assert_eq!(metrics.reconnects_total(), 5);
}
#[test]
fn apply_errors_are_not_reconnects() {
let metrics = ReplicaLinkMetrics::default();
metrics.observe_state("healthy");
metrics.observe_state("apply_error");
metrics.observe_state("divergence");
metrics.observe_state("relay_error");
metrics.observe_state("ack_error");
metrics.observe_state("healthy");
assert_eq!(metrics.reconnects_total(), 0);
}
#[test]
fn apply_error_during_outage_does_not_disarm_reconnect() {
let metrics = ReplicaLinkMetrics::default();
metrics.observe_state("healthy");
metrics.observe_state("connecting");
metrics.observe_state("apply_error");
metrics.observe_state("healthy");
assert_eq!(metrics.reconnects_total(), 1);
}
}