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("; "))
}
}
}