Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-web/src/admit.rs

//! **The Admission panel's words** — `GET /api/namespaces/{name}/admit`, the
//! plan gunnar's admission gate would make for this namespace's next transfer.
//!
//! Host-compiled for [`crate::activity`]'s reason: `pages::usage` is wasm-only,
//! so every sentence the panel decides lives here where `cargo test` runs it.
//! The per-kind sentence is gunnar's own and is shown verbatim; the only line
//! composed here is the budget line.

use serde::Deserialize;

use crate::fmt;

/// One kind of work's plan, as the BFF sends it.
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct AdmitKind {
    pub kind: String,
    pub class: String,
    pub why: String,
    /// `None` = gunnar has no estimate yet (never drawn as "0 B").
    pub estimate_bytes: Option<u64>,
    pub samples: u64,
}

/// The plan, as the BFF sends it.
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct AdmitPlan {
    pub namespace: String,
    pub policy: String,
    pub window_days: u32,
    pub used_bytes: u64,
    pub limit_bytes: Option<u64>,
    pub budget_left_bytes: Option<u64>,
    pub kinds: Vec<AdmitKind>,
}

/// **The budget line.** "No budget" is said as such — a box without the usage
/// share has none, and a `0` would read as "nothing left".
pub fn budget_line(plan: &AdmitPlan) -> String {
    let used = format!("{} moved in the last {} days", fmt::bytes(plan.used_bytes), plan.window_days);
    match (plan.limit_bytes, plan.budget_left_bytes) {
        (Some(limit), Some(0)) => format!(
            "{used} — over the {} budget: transfers are served at the reduced share until the window moves",
            fmt::bytes(limit)
        ),
        (Some(limit), Some(left)) => {
            format!("{used} of a {} budget; {} left at the full share", fmt::bytes(limit), fmt::bytes(left))
        }
        _ => format!("{used}; this box sets no transfer budget"),
    }
}

/// A kind's estimate, or the words for having none.
pub fn estimate_line(kind: &AdmitKind) -> String {
    match kind.estimate_bytes {
        Some(bytes) => format!("~{} per {} ({} measured)", fmt::bytes(bytes), kind.kind, kind.samples),
        None => format!("no estimate for a {} yet", kind.kind),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn plan(limit: Option<u64>, left: Option<u64>) -> AdmitPlan {
        AdmitPlan {
            namespace: "alpha".into(),
            policy: "usage-share".into(),
            window_days: 30,
            used_bytes: 2048,
            limit_bytes: limit,
            budget_left_bytes: left,
            kinds: vec![],
        }
    }

    #[test]
    fn no_budget_is_said_and_not_drawn_as_zero_left() {
        let line = budget_line(&plan(None, None));
        assert!(line.contains("sets no transfer budget"), "{line}");
        assert!(!line.contains(" 0 "), "{line}");
    }

    #[test]
    fn over_budget_says_what_happens() {
        assert!(budget_line(&plan(Some(1000), Some(0))).contains("reduced share"));
        assert!(budget_line(&plan(Some(1 << 20), Some(1 << 19))).contains("left at the full share"));
    }

    #[test]
    fn an_unlearnt_estimate_is_words_not_zero() {
        let k = AdmitKind { kind: "push".into(), class: "Full".into(), why: String::new(), estimate_bytes: None, samples: 0 };
        assert_eq!(estimate_line(&k), "no estimate for a push yet");
    }
}