use serde::{Deserialize, Serialize};
use super::{now_unix, LeanResult, Syncer};
const GAUGES: &str = "gauges.json";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LastBoundary {
pub source: String,
pub seq: u64,
pub unix: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Withheld {
Parked412,
}
impl Withheld {
pub fn as_str(&self) -> &'static str {
match self {
Withheld::Parked412 => "parked-412",
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Gauges {
pub rpo_secs: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub withheld_reason: Option<String>,
pub sentinel_budget_remaining: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_boundary: Option<LastBoundary>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_paused_since_unix: Option<u64>,
pub updated_unix: u64,
#[serde(default)]
pub last_durable_unix: u64,
}
impl Syncer {
fn gauges_path(&self) -> std::path::PathBuf {
self.cfg.state_dir().join(GAUGES)
}
pub fn load_gauges(&self) -> LeanResult<Gauges> {
let p = self.gauges_path();
if !p.exists() {
return Ok(Gauges::default());
}
Ok(std::fs::read(&p)
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_default())
}
pub fn note_boundary(&self, source: &str, seq: u64) -> LeanResult<()> {
let mut g = self.load_gauges()?;
g.last_boundary = Some(LastBoundary { source: source.into(), seq, unix: now_unix() });
g.last_durable_unix = now_unix();
self.save_gauges(&g)
}
pub fn note_auth_pause(&self) -> LeanResult<()> {
let mut g = self.load_gauges()?;
if g.auth_paused_since_unix.is_none() {
g.auth_paused_since_unix = Some(now_unix());
self.save_gauges(&g)?;
}
Ok(())
}
pub fn clear_auth_pause(&self) -> LeanResult<()> {
let mut g = self.load_gauges()?;
if g.auth_paused_since_unix.is_some() {
g.auth_paused_since_unix = None;
self.save_gauges(&g)?;
}
Ok(())
}
fn save_gauges(&self, g: &Gauges) -> LeanResult<()> {
let bytes = serde_json::to_vec_pretty(g)
.map_err(|e| super::LeanError::State(format!("gauges: {e}")))?;
super::control::write_atomic(&self.gauges_path(), &bytes)
}
pub fn write_gauges(&self, withheld: Option<Withheld>) -> LeanResult<Gauges> {
let now = now_unix();
let prev = self.load_gauges()?;
let budget = self.load_budget().unwrap_or_default();
let g = Gauges {
rpo_secs: if prev.last_durable_unix == 0 {
0
} else {
now.saturating_sub(prev.last_durable_unix)
},
withheld_reason: withheld.map(|w| w.as_str().to_string()),
sentinel_budget_remaining: budget.remaining(now, self.cfg.sentinel_hourly_budget),
last_boundary: prev.last_boundary.clone(),
updated_unix: now,
last_durable_unix: prev.last_durable_unix,
auth_paused_since_unix: prev.auth_paused_since_unix,
};
self.save_gauges(&g)?;
Ok(g)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct StatusReport {
pub root: String,
pub prefix: String,
pub gauges: Option<Gauges>,
pub capabilities: Option<super::control::Capabilities>,
pub remote_seq: Option<super::control::RemoteSeq>,
pub baseline_seq: u64,
pub incarnation_epoch: Option<u64>,
pub incarnation_holder: Option<String>,
pub pending_sentinels: Vec<String>,
pub recent_conflicts: usize,
pub checkout_complete: bool,
}
fn read_json<T: serde::de::DeserializeOwned>(p: &std::path::Path) -> Option<T> {
serde_json::from_slice(&std::fs::read(p).ok()?).ok()
}
pub fn status_report(cfg: &super::LeanConfig) -> LeanResult<StatusReport> {
let sd = cfg.state_dir();
let cd = cfg.control_dir();
let baseline: super::state::Baseline = read_json(&sd.join("baseline.json")).unwrap_or_default();
let inc: Option<super::state::Incarnation> = read_json(&sd.join("incarnation.json"));
let mut pending_sentinels = vec![];
for verb in [super::sentinel::Verb::Publish, super::sentinel::Verb::Sync] {
if sd.join(verb.pending_name()).exists() {
pending_sentinels.push(verb.sentinel_name().to_string());
}
}
let conflicts = std::fs::read_to_string(sd.join("conflicts.jsonl"))
.map(|s| s.lines().filter(|l| !l.trim().is_empty()).count())
.unwrap_or(0);
Ok(StatusReport {
root: cfg.root.display().to_string(),
prefix: cfg.prefix.clone(),
gauges: read_json(&sd.join(GAUGES)),
capabilities: read_json(&cd.join(super::control::CAPABILITIES)),
remote_seq: read_json(&cd.join(super::control::REMOTE_SEQ)),
baseline_seq: baseline.seq,
incarnation_epoch: inc.as_ref().map(|i| i.epoch),
incarnation_holder: inc.map(|i| i.holder_id),
pending_sentinels,
recent_conflicts: conflicts,
checkout_complete: sd.join("checkout-complete").exists(),
})
}