gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **A refusal that repeats because a caller retries is said once a minute** (release-3
//! test2, dev-narrate on: main logged `grpc.auth.refused reason="not-verified"` and the
//! matching `a control-plane call was refused … did not verify` at WARN 7/min from about
//! +04:16 — the front's certify thread (`Access.AddPrincipal`) and its console
//! (`Events.Watch`) calling with keys main had not installed yet; the same after a
//! recharge, on the blank main before its restore).
//!
//! A credential nobody registered is the one refusal a fresh or restored box answers for
//! a while by design, and every caller retries it. The FIRST of a shape in a window is
//! said at WARN; the rest of the window at INFO; the next window's first says how many
//! went quietly. Every other refusal is said as before.

use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};

/// One WARN of a shape per this long.
pub const SAY_EVERY: Duration = Duration::from_secs(60);

static WINDOWS: Mutex<Option<HashMap<String, (Instant, u64)>>> = Mutex::new(None);

/// **Is this refusal of `shape` the one said at WARN?** `Some(n)`: yes, and `n` went
/// quietly since the last one; `None`: said at INFO.
pub fn loud(shape: &str) -> Option<u64> {
    loud_at(shape, Instant::now())
}

/// [`loud`] at `now` (the tests' clock).
pub fn loud_at(shape: &str, now: Instant) -> Option<u64> {
    let mut g = WINDOWS.lock().unwrap_or_else(|p| p.into_inner());
    let map = g.get_or_insert_with(HashMap::new);
    match map.get_mut(shape) {
        Some((since, quiet)) if now.saturating_duration_since(*since) < SAY_EVERY => {
            *quiet += 1;
            None
        }
        Some(w) => {
            let went = w.1;
            *w = (now, 0);
            Some(went)
        }
        None => {
            map.insert(shape.to_owned(), (now, 0));
            Some(0)
        }
    }
}

/// The refusals quietened: a credential no key verifies (the key is not installed YET).
pub fn unverified(reason: &str) -> bool {
    reason == "not-verified"
}

/// **A call deferred by the data set's growth** (`grpc-status` 14): the box's
/// `[primary-growing]`, or the control store's `OffStripe` ("off the stripe
/// while the data set grows") — in the header's percent-encoding or plain.
pub fn growth_hold(grpc_status: Option<i32>, grpc_message: Option<&str>) -> bool {
    grpc_status == Some(14)
        && grpc_message.is_some_and(|m| m.contains("primary-growing") || m.replace("%20", " ").contains("off the stripe while the data set grows"))
}

/// ... and the same refusal as the response says it (`grpc-status` 16, the verifier's words).
pub fn unverified_answer(grpc_status: Option<i32>, grpc_message: Option<&str>) -> bool {
    grpc_status == Some(16) && grpc_message.is_some_and(|m| m.contains("did%20not%20verify") || m.contains("did not verify"))
}

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

    #[test]
    fn a_retried_unverified_refusal_is_said_once_a_minute_with_the_count_that_went_quietly() {
        let t0 = Instant::now();
        let shape = "test:a_retried_unverified_refusal";
        let mut loud_ones = Vec::new();
        // Seven in the first minute (the measured rate), seven in the second.
        for i in 0..14u64 {
            if let Some(n) = loud_at(shape, t0 + Duration::from_secs(i * 9)) {
                loud_ones.push((i, n));
            }
        }
        assert_eq!(loud_ones, vec![(0, 0), (7, 6)], "one WARN a minute, the next one names the six that went quietly");
        assert!(loud_at("test:another_shape", t0).is_some(), "shapes are counted apart");
        assert!(unverified("not-verified") && !unverified("replayed") && !unverified("stale"));
        assert!(unverified_answer(Some(16), Some("the%20credential%20did%20not%20verify%20against%20any%20registered%20key")));
        assert!(!unverified_answer(Some(7), Some("did not verify")) && !unverified_answer(Some(16), Some("malformed credential")));
    }
}