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

nordisk/holger

server/lib/src/ready.rs

//! # `/readyz` made real (lane B4, A31 §7.3)
//!
//! Kubernetes sends traffic to a pod only while its readiness probe answers 200.
//! Before this, `/readyz` answered a bare `ok` the moment the HTTP door bound,
//! so a server whose storage was not mounted, or whose scan sidecar had not
//! started, was handed every request and refused them all. Now the door asks
//! [`Readiness::check`] each time:
//!
//! | What | Ready when |
//! |---|---|
//! | Each repository's storage place | an archive is there; a files directory is there, or (a fresh repository, made on its first write) its nearest existing parent is a directory that takes a write |
//! | The data root (audit, the sealed store, the job ledger) | a file can be written there and removed |
//! | The scan sandbox, when this server scans | its engine answers (`Sandbox::ready`; a sidecar is pinged every time) |
//!
//! Every failure is named, all at once, so one probe line tells the operator
//! what to fix. `/healthz` stays the liveness probe and does not ask: a pod
//! whose sidecar is late is kept out of the Service, not restarted.

use std::path::PathBuf;
use std::sync::Arc;

/// The scan sandbox's own answer: `Ok`, or its refusal's words.
pub type SandboxCheck = Arc<dyn Fn() -> Result<(), String> + Send + Sync>;

/// One repository's storage place.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Place {
    /// An archive the repository serves from: it must be there.
    Archive(PathBuf),
    /// A files directory, made on the repository's first write: it is there, or
    /// it can be made.
    Files(PathBuf),
}

impl Place {
    fn check(&self) -> Result<(), String> {
        match self {
            Place::Archive(p) => std::fs::metadata(p)
                .map(|_| ())
                .map_err(|e| format!("{}: {e}", p.display())),
            Place::Files(p) if p.is_dir() => Ok(()),
            Place::Files(p) => {
                let parent = p
                    .ancestors()
                    .skip(1)
                    .find(|a| a.exists())
                    .ok_or_else(|| format!("{}: no parent exists", p.display()))?;
                if !parent.is_dir() {
                    return Err(format!(
                        "{}: {} is not a directory",
                        p.display(),
                        parent.display()
                    ));
                }
                write_probe(parent).map_err(|e| {
                    format!(
                        "{} cannot be made: {} takes no write: {e}",
                        p.display(),
                        parent.display()
                    )
                })
            }
        }
    }
}

fn write_probe(dir: &std::path::Path) -> std::io::Result<()> {
    let probe = dir.join(format!(".holger-readyz-{}", std::process::id()));
    std::fs::write(&probe, b"ready")?;
    let _ = std::fs::remove_file(&probe);
    Ok(())
}

/// What one `/readyz` asks.
#[derive(Clone)]
pub struct Readiness {
    /// `(repository, place)`: where each repository keeps its bytes.
    pub places: Vec<(String, Place)>,
    /// The data root: it must take a write.
    pub data_root: PathBuf,
    /// The scan sandbox, when this server scans; `None` when it does not.
    pub sandbox: Option<SandboxCheck>,
}

impl Readiness {
    /// `Ok`, or every reason this server is not ready, joined by `; `.
    pub fn check(&self) -> Result<(), String> {
        let mut why: Vec<String> = Vec::new();
        for (repo, place) in &self.places {
            if let Err(e) = place.check() {
                why.push(format!("storage `{repo}`: {e}"));
            }
        }
        if let Err(e) = write_probe(&self.data_root) {
            why.push(format!(
                "the data root {} takes no write: {e}",
                self.data_root.display()
            ));
        }
        if let Some(sb) = &self.sandbox {
            if let Err(e) = sb() {
                why.push(format!("sandbox: {e}"));
            }
        }
        if why.is_empty() {
            Ok(())
        } else {
            Err(why.join("; "))
        }
    }
}