use std::time::{Duration, Instant};
use super::CheckpointConfig;
#[cfg(unix)]
use super::{
CachedWalpinAttribution, WalpinFullScanPlan, DEFAULT_SESSION_SWEEP_INTERVAL,
DEFAULT_WALPIN_FULL_SCAN_INTERVAL,
};
#[derive(Debug)]
pub struct TruncateState {
pub(super) last_attempt: Option<Instant>,
pub(super) consecutive_failures: u32,
#[cfg(unix)]
pub(super) legacy_walpin_fallback_interval: Duration,
#[cfg(unix)]
pub(super) walpin_full_scan_interval: Duration,
#[cfg(unix)]
pub(super) walpin_full_scan_last_attempt: Option<Instant>,
#[cfg(unix)]
pub(super) walpin_cached_attribution: Option<CachedWalpinAttribution>,
#[cfg(unix)]
pub(super) sidecar_attribution_attempted_this_tick: bool,
}
impl Default for TruncateState {
fn default() -> Self {
Self {
last_attempt: None,
consecutive_failures: 0,
#[cfg(unix)]
legacy_walpin_fallback_interval: DEFAULT_SESSION_SWEEP_INTERVAL,
#[cfg(unix)]
walpin_full_scan_interval: DEFAULT_WALPIN_FULL_SCAN_INTERVAL,
#[cfg(unix)]
walpin_full_scan_last_attempt: None,
#[cfg(unix)]
walpin_cached_attribution: None,
#[cfg(unix)]
sidecar_attribution_attempted_this_tick: false,
}
}
}
impl TruncateState {
#[cfg(unix)]
pub(super) fn with_legacy_walpin_fallback(interval: Duration) -> Self {
Self {
legacy_walpin_fallback_interval: interval,
..Self::default()
}
}
#[cfg(all(test, unix))]
pub(super) fn with_walpin_full_scan_cadence(interval: Duration) -> Self {
Self {
walpin_full_scan_interval: interval,
..Self::default()
}
}
#[cfg(unix)]
pub(super) fn begin_tick(&mut self) {
self.sidecar_attribution_attempted_this_tick = false;
}
#[cfg(unix)]
pub(super) fn housekeeping_due(&self) -> bool {
!self.sidecar_attribution_attempted_this_tick
&& self.walpin_full_scan_due_at(Instant::now())
}
#[cfg(unix)]
fn walpin_full_scan_due_at(&self, now: Instant) -> bool {
self.walpin_full_scan_last_attempt.is_none_or(|last| {
now.saturating_duration_since(last) >= self.walpin_full_scan_interval
})
}
#[cfg(unix)]
pub(super) fn claim_walpin_full_scan_at(&mut self, now: Instant) -> bool {
if !self.walpin_full_scan_due_at(now) {
return false;
}
self.walpin_full_scan_last_attempt = Some(now);
true
}
#[cfg(unix)]
pub(super) fn plan_walpin_attribution_at(&mut self, now: Instant) -> WalpinFullScanPlan {
if self.walpin_full_scan_due_at(now) {
let previous_last_attempt = self.walpin_full_scan_last_attempt.replace(now);
WalpinFullScanPlan::Refresh {
previous_last_attempt,
}
} else if let Some(cached) = self.walpin_cached_attribution.clone() {
WalpinFullScanPlan::Cached(cached)
} else {
WalpinFullScanPlan::Suppressed
}
}
#[cfg(unix)]
pub(super) fn restore_walpin_full_scan_reservation(
&mut self,
previous_last_attempt: Option<Instant>,
) {
self.walpin_full_scan_last_attempt = previous_last_attempt;
}
#[cfg(unix)]
pub(super) fn cache_walpin_attribution(
&mut self,
report: crate::walpin::WalpinReport,
census: Result<crate::walpin::CensusResult, String>,
captured_at: Instant,
) {
self.walpin_cached_attribution = Some(CachedWalpinAttribution {
report,
census,
captured_at,
});
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckpointSeverityRung {
Info,
Warn,
Alarm,
}
#[derive(Debug, Default, Clone)]
pub struct CheckpointSeverityState {
was_above_warn: bool,
consecutive_above_warn: u8,
pub(super) warn_emitted_for_episode: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CheckpointSeverityEmission {
pub rung: CheckpointSeverityRung,
pub wal_pages: u64,
pub threshold_pages: u64,
pub consecutive_cycles: u8,
}
impl CheckpointSeverityState {
pub fn observe_wal_pages(
&mut self,
wal_pages: u64,
config: &CheckpointConfig,
) -> Vec<CheckpointSeverityEmission> {
let mut emissions = Vec::new();
let above_warn = wal_pages >= config.warn_pages;
if above_warn {
self.consecutive_above_warn = self.consecutive_above_warn.saturating_add(1);
if !self.was_above_warn {
emissions.push(CheckpointSeverityEmission {
rung: CheckpointSeverityRung::Info,
wal_pages,
threshold_pages: config.warn_pages,
consecutive_cycles: self.consecutive_above_warn,
});
}
if !self.warn_emitted_for_episode
&& self.consecutive_above_warn >= config.warn_sustained_cycles
{
emissions.push(CheckpointSeverityEmission {
rung: CheckpointSeverityRung::Warn,
wal_pages,
threshold_pages: config.warn_pages,
consecutive_cycles: self.consecutive_above_warn,
});
self.warn_emitted_for_episode = true;
}
} else {
self.consecutive_above_warn = 0;
self.warn_emitted_for_episode = false;
}
self.was_above_warn = above_warn;
emissions
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxAgeRung {
Warn,
Stale,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TxAgeEmission {
pub rung: TxAgeRung,
pub age: Duration,
pub label: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct TxAgeSweepState {
was_above_warn: bool,
was_above_max_age: bool,
tracked_id: Option<khive_storage::tx_registry::TxId>,
}
impl TxAgeSweepState {
pub fn observe(
&mut self,
oldest: Option<(khive_storage::tx_registry::TxId, Duration, Option<String>)>,
tx_warn_secs: Duration,
tx_max_age_secs: Duration,
) -> Vec<TxAgeEmission> {
let mut emissions = Vec::new();
let Some((id, age, label)) = oldest else {
self.was_above_warn = false;
self.was_above_max_age = false;
self.tracked_id = None;
return emissions;
};
if self.tracked_id != Some(id) {
self.was_above_warn = false;
self.was_above_max_age = false;
}
self.tracked_id = Some(id);
let above_warn = age >= tx_warn_secs;
let above_max_age = age >= tx_max_age_secs;
if above_warn && !self.was_above_warn {
emissions.push(TxAgeEmission {
rung: TxAgeRung::Warn,
age,
label: label.clone(),
});
}
if above_max_age && !self.was_above_max_age {
emissions.push(TxAgeEmission {
rung: TxAgeRung::Stale,
age,
label,
});
}
self.was_above_warn = above_warn;
self.was_above_max_age = above_max_age;
emissions
}
}