use anyhow::{Context, Result};
use clap::ValueEnum;
use crate::storage::{LockWait, MaintenanceOutcome, MaintenanceStatus};
const DEFAULT_MAINTENANCE_LOCK_WAIT_SECS: u64 = 120;
const MAINTENANCE_LOCK_WAIT_ENV: &str = "BOBBIN_MAINTENANCE_LOCK_WAIT_SECS";
pub(super) fn maintenance_lock_wait() -> LockWait {
let secs = std::env::var(MAINTENANCE_LOCK_WAIT_ENV)
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.unwrap_or(DEFAULT_MAINTENANCE_LOCK_WAIT_SECS);
if secs == 0 {
LockWait::NoWait
} else {
LockWait::UpTo(std::time::Duration::from_secs(secs))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum MaintenanceMode {
#[default]
Always,
Auto,
Never,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Decision {
Run,
SkipRecent {
age_secs: u64,
},
SkipDisabled,
}
pub(super) fn decide(
mode: MaintenanceMode,
force: bool,
status: MaintenanceStatus,
now_unix: u64,
interval_secs: u64,
) -> Decision {
match mode {
MaintenanceMode::Always => Decision::Run,
MaintenanceMode::Never => Decision::SkipDisabled,
MaintenanceMode::Auto if force => Decision::Run,
MaintenanceMode::Auto => match status.last_compact_unix {
Some(last) if last <= now_unix && now_unix - last < interval_secs => {
Decision::SkipRecent {
age_secs: now_unix - last,
}
}
_ => Decision::Run,
},
}
}
#[derive(Debug)]
pub(super) struct MaintenanceReport {
outcome: Result<MaintenanceOutcome, Decision>,
}
impl MaintenanceReport {
pub(super) fn from_result(r: anyhow::Result<MaintenanceOutcome>) -> anyhow::Result<Self> {
Ok(Self {
outcome: Ok(r.context("Lance maintenance failed")?),
})
}
pub(super) fn skipped(decision: Decision) -> Self {
Self {
outcome: Err(decision),
}
}
pub(super) fn warn_if_starved(&self) {
let Ok(MaintenanceOutcome::SkippedLockHeld { waited }) = self.outcome else {
return;
};
let waited = waited.as_secs();
eprintln!(
"warning: MAINTENANCE SKIPPED — another process held the store maintenance \
lock for the whole {waited}s wait. Nothing was pruned or compacted by this \
run; the store did not shrink. Set {MAINTENANCE_LOCK_WAIT_ENV} higher, or \
find the contender (fuser .maintenance.lock in the lance dir)."
);
}
pub(super) fn json_label(&self) -> String {
match self.outcome {
Ok(outcome) => outcome.label().to_string(),
Err(Decision::SkipRecent { .. }) => "skipped_recent".to_string(),
Err(Decision::SkipDisabled) => "skipped_disabled".to_string(),
Err(Decision::Run) => "ran".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const HOUR: u64 = 3600;
const NOW: u64 = 1_792_161_137;
fn compacted_at(t: u64) -> MaintenanceStatus {
MaintenanceStatus {
last_compact_unix: Some(t),
last_prune_unix: Some(t),
}
}
#[test]
fn always_runs_whatever_the_record_says() {
let fresh = compacted_at(NOW - 60);
assert_eq!(
decide(MaintenanceMode::Always, false, fresh, NOW, 24 * HOUR),
Decision::Run
);
}
#[test]
fn auto_skips_a_recent_compaction_and_reports_its_age() {
let d = decide(
MaintenanceMode::Auto,
false,
compacted_at(NOW - 900),
NOW,
24 * HOUR,
);
assert_eq!(d, Decision::SkipRecent { age_secs: 900 });
assert_eq!(MaintenanceReport::skipped(d).json_label(), "skipped_recent");
}
#[test]
fn auto_runs_once_the_interval_has_passed() {
let d = decide(
MaintenanceMode::Auto,
false,
compacted_at(NOW - 24 * HOUR),
NOW,
24 * HOUR,
);
assert_eq!(d, Decision::Run);
}
#[test]
fn auto_runs_when_the_store_was_never_compacted() {
let d = decide(
MaintenanceMode::Auto,
false,
MaintenanceStatus::default(),
NOW,
24 * HOUR,
);
assert_eq!(d, Decision::Run);
}
#[test]
fn auto_treats_a_future_stamp_as_stale_not_fresh() {
let d = decide(
MaintenanceMode::Auto,
false,
compacted_at(NOW + HOUR),
NOW,
24 * HOUR,
);
assert_eq!(d, Decision::Run);
}
#[test]
fn force_makes_auto_sweep_even_after_a_recent_compaction() {
let d = decide(
MaintenanceMode::Auto,
true,
compacted_at(NOW - 60),
NOW,
24 * HOUR,
);
assert_eq!(d, Decision::Run);
}
#[test]
fn never_skips_even_on_force_and_says_so() {
let d = decide(
MaintenanceMode::Never,
true,
MaintenanceStatus::default(),
NOW,
24 * HOUR,
);
assert_eq!(d, Decision::SkipDisabled);
assert_eq!(
MaintenanceReport::skipped(d).json_label(),
"skipped_disabled"
);
}
#[test]
fn a_deliberate_skip_is_never_reported_as_lock_starved() {
let report = MaintenanceReport::skipped(Decision::SkipRecent { age_secs: 1 });
assert!(!matches!(
report.outcome,
Ok(MaintenanceOutcome::SkippedLockHeld { .. })
));
}
}