use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use anyhow::{Context, Result};
use crate::layout_config::LayoutConfig;
pub const REPOS_DIR: &str = "repos";
pub const META_DIR: &str = "meta";
pub const CRASH_DIR: &str = "crash";
pub const LOG_DIR: &str = "log";
pub const GC_DIR: &str = "gc";
pub const QUARANTINE_DIR: &str = "quarantine";
pub fn shown_path(configured: &Path) -> String {
match std::fs::canonicalize(configured) {
Ok(real) if real != configured => format!("{} (really {})", configured.display(), real.display()),
_ => configured.display().to_string(),
}
}
pub const TARGET_SLOW: &str = "gunnar::slow";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Placement {
pub what: &'static str,
pub dir: &'static str,
pub requires_off_stripe: bool,
pub wired: bool,
pub note: &'static str,
}
pub const PLACEMENT: &[Placement] = &[
Placement {
what: "git objects, packfiles, LFS, refs",
dir: REPOS_DIR,
requires_off_stripe: false,
wired: true,
note: "",
},
Placement {
what: "metadata mirror (roster, principals, enrolled keys)",
dir: META_DIR,
requires_off_stripe: true,
wired: true,
note: "",
},
Placement {
what: "crash + diagnostics",
dir: CRASH_DIR,
requires_off_stripe: true,
wired: true,
note: "",
},
Placement {
what: "audit / console log",
dir: LOG_DIR,
requires_off_stripe: true,
wired: true,
note: "",
},
Placement {
what: "git gc / repack scratch",
dir: GC_DIR,
requires_off_stripe: false,
wired: false,
note: "the storage engine chooses its own temporaries; pointing them here is an \
engine change, not a placement one",
},
Placement {
what: "push quarantine",
dir: QUARANTINE_DIR,
requires_off_stripe: false,
wired: false,
note: "moving the incoming-*.pack landing spot off the stripe turns the final rename \
into a whole-pack cross-device copy on the push path; measure before trading",
},
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Claim {
OffStripe,
SameDevice,
Absent,
}
impl Claim {
pub fn word(self) -> &'static str {
match self {
Claim::OffStripe => "off-stripe",
Claim::SameDevice => "same-device",
Claim::Absent => "absent",
}
}
pub fn satisfied(self) -> bool {
matches!(self, Claim::OffStripe)
}
pub fn warns(self) -> bool {
!self.satisfied()
}
pub fn from_satisfied(satisfied: Option<bool>) -> Claim {
match satisfied {
Some(true) => Claim::OffStripe,
Some(false) => Claim::SameDevice,
None => Claim::Absent,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Source {
Config,
Fallback,
}
impl Source {
pub fn word(self) -> &'static str {
match self {
Source::Config => "named by the layout config",
Source::Fallback => {
"no layout config: the documented default under the data root, and nothing \
here went looking for a slow area"
}
}
}
}
#[derive(Clone, Debug)]
pub struct SlowRoot {
data_root: PathBuf,
source: Source,
dirs: BTreeMap<&'static str, PathBuf>,
claims: BTreeMap<&'static str, Claim>,
serialdat: String,
class: String,
device: String,
}
impl SlowRoot {
pub fn resolve(data_root: &Path, config: Option<&LayoutConfig>) -> SlowRoot {
let source = if config.is_some() {
Source::Config
} else {
Source::Fallback
};
let mut dirs = BTreeMap::new();
let mut claims = BTreeMap::new();
for p in PLACEMENT {
let path = config
.and_then(|c| c.dir_path(p.dir))
.unwrap_or_else(|| data_root.join(p.dir));
dirs.insert(p.dir, path);
let claim = if p.requires_off_stripe {
Claim::from_satisfied(config.and_then(|c| c.dir_off_stripe_satisfied(p.dir)))
} else {
Claim::Absent
};
claims.insert(p.dir, claim);
}
let field = |key: &str| {
config
.and_then(|c| c.get(key))
.unwrap_or_default()
.to_owned()
};
SlowRoot {
data_root: data_root.to_path_buf(),
source,
dirs,
claims,
serialdat: field("serialdat.mountpoint"),
class: field("serialdat.class"),
device: field("serialdat.dev"),
}
}
pub fn source(&self) -> Source {
self.source
}
pub fn serialdat(&self) -> &str {
&self.serialdat
}
pub fn class(&self) -> &str {
&self.class
}
pub fn device(&self) -> &str {
&self.device
}
pub fn path_of(&self, name: &str) -> PathBuf {
self.dirs
.get(name)
.cloned()
.unwrap_or_else(|| self.data_root.join(name))
}
pub fn claim(&self) -> Claim {
let mut worst = Claim::OffStripe;
let mut seen = false;
for p in PLACEMENT.iter().filter(|p| p.requires_off_stripe) {
seen = true;
let c = self.claim_of(p.dir);
worst = match (worst, c) {
(Claim::SameDevice, _) | (_, Claim::SameDevice) => Claim::SameDevice,
(Claim::Absent, _) | (_, Claim::Absent) => Claim::Absent,
_ => Claim::OffStripe,
};
}
if seen {
worst
} else {
Claim::Absent
}
}
pub fn claim_of(&self, name: &str) -> Claim {
self.claims.get(name).copied().unwrap_or(Claim::Absent)
}
pub fn dir(&self, name: &str) -> Result<PathBuf> {
let dir = self.path_of(name);
std::fs::create_dir_all(&dir)
.with_context(|| format!("creating the configured directory {}", dir.display()))?;
Ok(dir)
}
pub fn meta_dir(&self) -> Result<PathBuf> {
self.dir(META_DIR)
}
pub fn crash_dir(&self) -> Result<PathBuf> {
self.dir(CRASH_DIR)
}
pub fn log_dir(&self) -> Result<PathBuf> {
self.dir(LOG_DIR)
}
pub fn gc_dir(&self) -> Result<PathBuf> {
self.dir(GC_DIR)
}
pub fn quarantine_dir(&self) -> Result<PathBuf> {
self.dir(QUARANTINE_DIR)
}
pub fn health_line(&self, what: &str, dir: &str) -> String {
let where_ = shown_path(&self.path_of(dir));
match self.claim_of(dir) {
Claim::OffStripe => format!(
"{what}: present at {}, off-stripe ({}) — survives losing a data disk",
where_,
self.backing_phrase()
),
Claim::SameDevice => format!(
"{what}: present at {}, SAME DEVICE as the data it protects ({}) — NOT \
off-stripe; it does not survive losing a data disk",
where_,
self.backing_phrase()
),
Claim::Absent => format!(
"{what}: present at {}, and nothing says what it is on ({}) — NOT confirmed \
off-stripe; treat it as same-device until the layout config says otherwise",
where_,
self.backing_phrase()
),
}
}
pub fn health(&self) -> Vec<String> {
PLACEMENT
.iter()
.filter(|p| p.requires_off_stripe)
.map(|p| self.health_line(p.what, p.dir))
.collect()
}
fn backing_phrase(&self) -> String {
match (self.class.as_str(), self.device.as_str()) {
("", _) => "no layout config".to_owned(),
(class, "") => class.to_owned(),
(class, dev) => format!("{class} {dev}"),
}
}
pub fn row(&self) -> (String, bool) {
let claim = self.claim();
let text = match claim {
Claim::OffStripe => format!("off-stripe: {}", self.backing_phrase()),
Claim::SameDevice => format!(
"SAME DEVICE as /data \u{2014} not off-stripe ({})",
self.backing_phrase()
),
Claim::Absent => format!(
"nothing says what these are on \u{2014} not confirmed off-stripe ({})",
self.backing_phrase()
),
};
(text, claim.warns())
}
pub fn announce(&self, config_path: &str, authenticity: &str) {
let (text, warn) = self.row();
let text = if authenticity.is_empty() {
text
} else {
format!("{authenticity} \u{b7} {text}")
};
if warn {
tracing::warn!(
target: TARGET_SLOW,
event = "layout",
path = config_path,
claim = self.claim().word(),
source = self.source.word(),
"{text}"
);
} else {
tracing::info!(
target: TARGET_SLOW,
event = "layout",
path = config_path,
claim = self.claim().word(),
source = self.source.word(),
"{text}"
);
}
}
}
static CRASH_DIR_REGISTERED: OnceLock<PathBuf> = OnceLock::new();
pub fn register_crash_dir(dir: PathBuf) {
let _ = CRASH_DIR_REGISTERED.set(dir);
}
pub fn record_crash(text: &str) -> Option<PathBuf> {
let dir = CRASH_DIR_REGISTERED.get()?;
let stamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or_default();
let path = dir.join(format!("panic-{stamp}.txt"));
std::fs::create_dir_all(dir).ok()?;
std::fs::write(&path, text.as_bytes()).ok()?;
Some(path)
}
pub fn adopt_file(old: &Path, new: &Path) -> PathBuf {
if new.exists() || !old.exists() {
return new.to_path_buf();
}
let Some(parent) = new.parent() else {
return old.to_path_buf();
};
if std::fs::create_dir_all(parent).is_err() {
return old.to_path_buf();
}
let temp = new.with_extension("adopting");
let copied = std::fs::copy(old, &temp).is_ok()
&& std::fs::File::open(&temp)
.and_then(|f| f.sync_all())
.is_ok()
&& std::fs::rename(&temp, new).is_ok();
if !copied {
let _ = std::fs::remove_file(&temp);
return old.to_path_buf();
}
if let Ok(dir) = std::fs::File::open(parent) {
let _ = dir.sync_all();
}
if let Err(err) = std::fs::remove_file(old) {
tracing::warn!(
target: TARGET_SLOW,
old = %old.display(),
new = %new.display(),
%err,
"the file was copied to its configured directory but the old copy could not be \
removed; the one in use is the new one"
);
}
new.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout_config::{Authenticity, LayoutConfig};
fn config(satisfied: bool, class: &str, dev: &str) -> LayoutConfig {
let yn = if satisfied { "yes" } else { "no" };
let mut text = format!(
"korp-layout-config 1\n\
data.mountpoint=/data\n\
serialdat.mountpoint=/serialdat\n\
serialdat.class={class}\n\
serialdat.dev={dev}\n\
serialdat.off_stripe={yn}\n\
dir.users_dir=repos\n\
dir.repos.path=/data/repos\n\
dir.repos.off_stripe_required=no\n\
dir.repos.off_stripe_satisfied=yes\n\
dir.meta.path=/serialdat/meta\n\
dir.meta.off_stripe_required=yes\n\
dir.meta.off_stripe_satisfied={yn}\n"
);
for d in [CRASH_DIR, LOG_DIR] {
text.push_str(&format!(
"dir.{d}.path=/data/{d}\n\
dir.{d}.off_stripe_required=yes\n\
dir.{d}.off_stripe_satisfied={yn}\n"
));
}
for d in [GC_DIR, QUARANTINE_DIR] {
text.push_str(&format!(
"dir.{d}.path=/data/{d}\n\
dir.{d}.off_stripe_required=no\n\
dir.{d}.off_stripe_satisfied=yes\n"
));
}
LayoutConfig::parse(
PathBuf::from("/boot/korp/layout.conf"),
&text,
Authenticity::IntegrityOnly,
)
.expect("the fixture parses")
}
#[cfg(unix)]
#[test]
fn a_symlinked_directory_is_shown_with_where_it_really_is() {
let t = tempfile::tempdir().unwrap();
let root = t.path().canonicalize().unwrap();
let (data, slow) = (root.join("data"), root.join("serialdat"));
std::fs::create_dir_all(&data).unwrap();
std::fs::create_dir_all(slow.join("crash")).unwrap();
std::os::unix::fs::symlink(slow.join("crash"), data.join("crash")).unwrap();
assert_eq!(shown_path(&data.join("crash")), format!("{} (really {})", data.join("crash").display(), slow.join("crash").display()));
assert_eq!(shown_path(&data), data.display().to_string());
assert_eq!(shown_path(Path::new("/nowhere/at/all")), "/nowhere/at/all");
}
#[test]
fn every_path_comes_from_the_config_unchanged() {
let c = config(true, "p3", "/dev/vda3");
let s = SlowRoot::resolve(Path::new("/data"), Some(&c));
assert_eq!(s.path_of(LOG_DIR), PathBuf::from("/data/log"));
assert_eq!(s.path_of(CRASH_DIR), PathBuf::from("/data/crash"));
assert_eq!(s.path_of(REPOS_DIR), PathBuf::from("/data/repos"));
assert_eq!(s.path_of(META_DIR), PathBuf::from("/serialdat/meta"));
assert_eq!(s.source(), Source::Config);
}
#[test]
fn a_config_missing_one_dir_falls_back_for_that_one_only() {
let text = "korp-layout-config 1\ndir.meta.path=/serialdat/meta\n\
dir.meta.off_stripe_required=yes\ndir.meta.off_stripe_satisfied=yes\n";
let c = LayoutConfig::parse(PathBuf::from("/x"), text, Authenticity::IntegrityOnly)
.expect("parses");
let s = SlowRoot::resolve(Path::new("/data"), Some(&c));
assert_eq!(s.path_of(META_DIR), PathBuf::from("/serialdat/meta"));
assert_eq!(s.path_of(LOG_DIR), PathBuf::from("/data/log"));
}
#[test]
fn a_satisfied_config_claims_off_stripe_and_does_not_warn() {
let c = config(true, "p3", "/dev/vda3");
let s = SlowRoot::resolve(Path::new("/data"), Some(&c));
assert_eq!(s.claim(), Claim::OffStripe);
assert!(s.claim().satisfied());
let (text, warn) = s.row();
assert!(!warn, "a real spindle is not a warning: {text}");
assert!(text.contains("off-stripe: p3 /dev/vda3"), "{text}");
}
#[test]
fn a_same_device_layout_is_never_reported_as_satisfied() {
let c = config(false, "fallback", "/data");
let s = SlowRoot::resolve(Path::new("/data"), Some(&c));
assert_eq!(s.claim(), Claim::SameDevice);
assert!(!s.claim().satisfied(), "a mirror that dies with its original satisfies nothing");
assert!(s.claim().warns());
assert_eq!(s.claim().word(), "same-device");
let lines = s.health();
assert_eq!(lines.len(), 3, "three copies need off-stripe");
for line in lines {
let lower = line.to_lowercase();
assert!(
lower.contains("present"),
"it IS there, and saying `missing` sends someone looking for a bug: {line}"
);
assert!(
lower.contains("not off-stripe"),
"the sentence has to carry the limitation: {line}"
);
assert!(
!lower.contains(" ok"),
"`OK` for a same-device mirror is the lie this module exists to stop: {line}"
);
}
let (text, warn) = s.row();
assert!(warn, "{text}");
assert!(text.contains("SAME DEVICE"), "{text}");
}
#[test]
fn without_a_config_everything_is_absent_and_nothing_is_inferred() {
let s = SlowRoot::resolve(Path::new("/var/lib/gunnar"), None);
assert_eq!(s.source(), Source::Fallback);
assert_eq!(s.claim(), Claim::Absent);
assert_eq!(s.claim().word(), "absent");
assert!(!s.claim().satisfied());
assert_eq!(s.path_of(META_DIR), PathBuf::from("/var/lib/gunnar/meta"));
assert_eq!(s.path_of(LOG_DIR), PathBuf::from("/var/lib/gunnar/log"));
assert_eq!(s.serialdat(), "");
assert_eq!(s.class(), "");
assert_eq!(s.device(), "");
for line in s.health() {
assert!(line.contains("present"), "{line}");
assert!(
line.to_lowercase().contains("not confirmed off-stripe"),
"{line}"
);
}
}
#[test]
fn a_disagreeing_config_summarises_downwards() {
let mut text = String::from("korp-layout-config 1\n");
for (d, yn) in [(META_DIR, "yes"), (CRASH_DIR, "yes"), (LOG_DIR, "no")] {
text.push_str(&format!(
"dir.{d}.path=/data/{d}\ndir.{d}.off_stripe_required=yes\n\
dir.{d}.off_stripe_satisfied={yn}\n"
));
}
let c = LayoutConfig::parse(PathBuf::from("/x"), &text, Authenticity::IntegrityOnly)
.expect("parses");
let s = SlowRoot::resolve(Path::new("/data"), Some(&c));
assert_eq!(s.claim_of(META_DIR), Claim::OffStripe);
assert_eq!(s.claim_of(LOG_DIR), Claim::SameDevice);
assert_eq!(
s.claim(),
Claim::SameDevice,
"one copy that cannot survive a disk makes the summary same-device"
);
}
#[test]
fn the_placement_table_holds() {
let need: Vec<_> = PLACEMENT.iter().filter(|p| p.requires_off_stripe).collect();
assert_eq!(
need.iter().map(|p| p.dir).collect::<Vec<_>>(),
[META_DIR, CRASH_DIR, LOG_DIR]
);
assert!(
need.iter().all(|p| p.wired),
"a copy that must survive a disk and is not written is not a placement"
);
assert_eq!(PLACEMENT[0].dir, REPOS_DIR);
assert!(!PLACEMENT[0].requires_off_stripe);
let unwired: Vec<_> = PLACEMENT.iter().filter(|p| !p.wired).collect();
assert_eq!(
unwired.iter().map(|p| p.dir).collect::<Vec<_>>(),
[GC_DIR, QUARANTINE_DIR]
);
assert!(
unwired.iter().all(|p| !p.note.is_empty()),
"an unwired row without a reason is a directory pretending to be a feature"
);
assert!(PLACEMENT.iter().all(|p| p.dir != "typeset" && p.dir != "generations"));
}
#[test]
fn the_layout_is_identical_in_every_state() {
let paths = |c: Option<&LayoutConfig>| {
let s = SlowRoot::resolve(Path::new("/data"), c);
PLACEMENT
.iter()
.map(|p| s.path_of(p.dir))
.collect::<Vec<_>>()
};
let satisfied = config(true, "p3", "/dev/vda3");
let not = config(false, "fallback", "/data");
assert_eq!(
paths(Some(&satisfied)),
paths(Some(¬)),
"the verdict must not move one byte of the layout"
);
}
#[test]
fn every_directory_is_created_on_demand_under_the_configured_path() {
let dir = tempfile::tempdir().expect("a temp dir");
let s = SlowRoot::resolve(dir.path(), None);
for d in [
s.meta_dir().expect("meta"),
s.crash_dir().expect("crash"),
s.log_dir().expect("log"),
s.gc_dir().expect("gc"),
s.quarantine_dir().expect("quarantine"),
] {
assert!(d.is_dir(), "{} was not created", d.display());
assert!(d.starts_with(dir.path()), "{}", d.display());
}
}
#[test]
fn adopting_moves_the_file_once_and_leaves_nothing_behind() {
let dir = tempfile::tempdir().expect("a temp dir");
let old = dir.path().join("control/audit-chain.jsonl");
std::fs::create_dir_all(old.parent().unwrap()).expect("mkdir");
std::fs::write(&old, b"{\"seq\":0}\n").expect("write");
let new = dir.path().join("log/audit-chain.jsonl");
assert_eq!(adopt_file(&old, &new), new);
assert_eq!(std::fs::read(&new).expect("the moved file"), b"{\"seq\":0}\n");
assert!(!old.exists(), "the old copy is gone");
assert!(!new.with_extension("adopting").exists());
std::fs::write(&new, b"{\"seq\":1}\n").expect("append");
assert_eq!(adopt_file(&old, &new), new);
assert_eq!(std::fs::read(&new).expect("untouched"), b"{\"seq\":1}\n");
}
#[test]
fn adopting_nothing_still_names_the_new_path() {
let dir = tempfile::tempdir().expect("a temp dir");
let old = dir.path().join("control/audit-chain.jsonl");
let new = dir.path().join("log/audit-chain.jsonl");
assert_eq!(adopt_file(&old, &new), new, "a fresh box starts in the right place");
}
#[test]
fn adopting_into_an_impossible_place_keeps_the_original() {
let dir = tempfile::tempdir().expect("a temp dir");
let old = dir.path().join("audit-chain.jsonl");
std::fs::write(&old, b"x").expect("write");
let blocker = dir.path().join("blocker");
std::fs::write(&blocker, b"not a directory").expect("write");
let new = blocker.join("log/audit-chain.jsonl");
assert_eq!(adopt_file(&old, &new), old);
assert!(old.exists(), "the original is untouched");
}
}