use std::path::Path;
use anyhow::Context as _;
use super::write::{self, Durability};
pub(crate) struct ServingPointer<'a> {
root: &'a Path,
}
impl<'a> ServingPointer<'a> {
pub(super) fn new(root: &'a Path) -> Self {
Self { root }
}
pub(crate) fn generation(&self) -> Option<String> {
let link = std::fs::read_link(self.root.join("current")).ok()?;
link.file_name()
.and_then(|name| name.to_str())
.map(str::to_string)
}
/// the generation must be durable first, or a power cut leaves this pointing at empty files.
pub(crate) fn point_at(&self, generation: &str) -> anyhow::Result<()> {
let current = self.root.join("current");
let staging = self
.root
.join(format!(".current.tmp.{}", std::process::id()));
let _ = std::fs::remove_file(&staging);
let target = Path::new("generations").join(generation);
std::os::unix::fs::symlink(&target, &staging)
.with_context(|| format!("symlink {} -> {}", staging.display(), target.display()))?;
std::fs::rename(&staging, ¤t)
.with_context(|| format!("rename symlink to {}", current.display()))?;
write::sync_directory(self.root, Durability::Fsync)
.with_context(|| format!("sync {}", self.root.display()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pointing_at_a_generation_syncs_the_rename_and_leaves_no_staging_link() {
let root = tempfile::tempdir().unwrap();
let pointer = ServingPointer::new(root.path());
assert_eq!(pointer.generation(), None);
let before = write::syncs();
pointer.point_at("gen-1").unwrap();
assert_eq!(pointer.generation().as_deref(), Some("gen-1"));
assert_eq!(
write::syncs(),
before + 1,
"the directory the pointer was renamed in"
);
pointer.point_at("gen-2").unwrap();
assert_eq!(pointer.generation().as_deref(), Some("gen-2"));
let staging: Vec<_> = std::fs::read_dir(root.path())
.unwrap()
.map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned())
.filter(|name| name.contains(".tmp."))
.collect();
assert!(staging.is_empty(), "staging link left behind: {staging:?}");
}
}