remove the partial salvage dir when a salvage fails
- a failure between creating the dir and the rename removes that dir only - give the ephemeral-rootfs vm test a 4 GiB disk so the space check passes
This commit is contained in:
parent
836b15a46e
commit
86e80250bb
3 changed files with 115 additions and 13 deletions
|
|
@ -423,9 +423,10 @@ anything, hive-priv sizes the root and compares it with the free space under
|
||||||
leave 2 GiB free. One that has to copy data must leave 15 % of the filesystem
|
leave 2 GiB free. One that has to copy data must leave 15 % of the filesystem
|
||||||
free. When the free space is short, hive-priv skips the salvage, deletes
|
free. When the free space is short, hive-priv skips the salvage, deletes
|
||||||
nothing, logs `salvage skipped: <container> need <size> free <size>`, and the
|
nothing, logs `salvage skipped: <container> need <size> free <size>`, and the
|
||||||
container starts persistent; the next start tries again. hive-priv checks a file that fails to
|
container starts persistent; the next start tries again. hive-priv checks a file
|
||||||
clone against the 2 GiB reserve again before it copies the data. After a
|
that fails to clone against the 2 GiB reserve again before it copies the data. A
|
||||||
salvage, hive-priv logs `salvage: <container> <size> reflinked=<n>/<m> files`.
|
salvage that fails partway removes the directory it created, and leaves the root
|
||||||
|
and any earlier salvage as they were. After a salvage, hive-priv logs `salvage: <container> <size> reflinked=<n>/<m> files`.
|
||||||
|
|
||||||
**Concurrency.** hive-priv holds an `flock` on
|
**Concurrency.** hive-priv holds an `flock` on
|
||||||
`/run/hive-priv/salvage-<container>.lock` (the unit's `RuntimeDirectory`, which
|
`/run/hive-priv/salvage-<container>.lock` (the unit's `RuntimeDirectory`, which
|
||||||
|
|
|
||||||
|
|
@ -243,16 +243,8 @@ fn salvage(
|
||||||
return Ok(Outcome::Skipped { need, free });
|
return Ok(Outcome::Skipped { need, free });
|
||||||
}
|
}
|
||||||
let salvage = fresh_dir(work, &format!("salvaged-rootfs-{date}"))?;
|
let salvage = fresh_dir(work, &format!("salvaged-rootfs-{date}"))?;
|
||||||
lchown(&salvage, Some(uid), Some(gid))
|
let copied = copy_and_move_aside(root, &salvage, agent, profiles, date, env, (uid, gid));
|
||||||
.with_context(|| format!("chown {}", salvage.display()))?;
|
let (old_root, stats) = copied.inspect_err(|_| discard_partial(&salvage))?;
|
||||||
let mut copier = Copier::new(env.backend, uid, gid);
|
|
||||||
copier
|
|
||||||
.tree(root, &salvage, Path::new(""))
|
|
||||||
.with_context(|| format!("copy {} to {}", root.display(), salvage.display()))?;
|
|
||||||
carry_nix_state(root, agent, profiles, env.backend, uid, gid)?;
|
|
||||||
let old_root = free_sibling(root, &format!("pre-ephemeral-{date}"))?;
|
|
||||||
std::fs::rename(root, &old_root)
|
|
||||||
.with_context(|| format!("move {} to {}", root.display(), old_root.display()))?;
|
|
||||||
std::fs::create_dir(root).with_context(|| format!("create {}", root.display()))?;
|
std::fs::create_dir(root).with_context(|| format!("create {}", root.display()))?;
|
||||||
if let Some(id) = machine_id {
|
if let Some(id) = machine_id {
|
||||||
write_container_etc_file_in(root, "machine-id", &format!("{id}\n"))?;
|
write_container_etc_file_in(root, "machine-id", &format!("{id}\n"))?;
|
||||||
|
|
@ -260,10 +252,47 @@ fn salvage(
|
||||||
Ok(Outcome::Salvaged {
|
Ok(Outcome::Salvaged {
|
||||||
salvage,
|
salvage,
|
||||||
old_root,
|
old_root,
|
||||||
stats: copier.stats,
|
stats,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copy `root` into the fresh `salvage` dir, then rename `root` aside; returns
|
||||||
|
/// where it went. Up to the rename the old root is untouched, so a failure
|
||||||
|
/// anywhere in here leaves it where it was.
|
||||||
|
fn copy_and_move_aside(
|
||||||
|
root: &Path,
|
||||||
|
salvage: &Path,
|
||||||
|
agent: &str,
|
||||||
|
profiles: &Path,
|
||||||
|
date: &str,
|
||||||
|
env: &Env<'_>,
|
||||||
|
(uid, gid): (u32, u32),
|
||||||
|
) -> Result<(PathBuf, Stats)> {
|
||||||
|
lchown(salvage, Some(uid), Some(gid))
|
||||||
|
.with_context(|| format!("chown {}", salvage.display()))?;
|
||||||
|
let mut copier = Copier::new(env.backend, uid, gid);
|
||||||
|
copier
|
||||||
|
.tree(root, salvage, Path::new(""))
|
||||||
|
.with_context(|| format!("copy {} to {}", root.display(), salvage.display()))?;
|
||||||
|
carry_nix_state(root, agent, profiles, env.backend, uid, gid)?;
|
||||||
|
let old_root = free_sibling(root, &format!("pre-ephemeral-{date}"))?;
|
||||||
|
std::fs::rename(root, &old_root)
|
||||||
|
.with_context(|| format!("move {} to {}", root.display(), old_root.display()))?;
|
||||||
|
Ok((old_root, copier.stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the directory a failed salvage was filling: the one this attempt
|
||||||
|
/// created, never an earlier salvage. `remove_dir_all` does not follow links.
|
||||||
|
fn discard_partial(salvage: &Path) {
|
||||||
|
if let Err(e) = std::fs::remove_dir_all(salvage) {
|
||||||
|
tracing::warn!(
|
||||||
|
salvage = %salvage.display(),
|
||||||
|
error = %e,
|
||||||
|
"could not remove the partial salvage; remove it by hand"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What [`measure`] found under a root.
|
/// What [`measure`] found under a root.
|
||||||
struct Tally {
|
struct Tally {
|
||||||
/// Everything outside [`CACHE`], directories and links included.
|
/// Everything outside [`CACHE`], directories and links included.
|
||||||
|
|
@ -752,6 +781,8 @@ mod tests {
|
||||||
total: u64,
|
total: u64,
|
||||||
mode: CloneMode,
|
mode: CloneMode,
|
||||||
delay: Duration,
|
delay: Duration,
|
||||||
|
space_calls: AtomicU32,
|
||||||
|
space_fails_from: u32,
|
||||||
calls: AtomicU32,
|
calls: AtomicU32,
|
||||||
started: AtomicBool,
|
started: AtomicBool,
|
||||||
active: AtomicU32,
|
active: AtomicU32,
|
||||||
|
|
@ -766,6 +797,8 @@ mod tests {
|
||||||
total: 1 << 51,
|
total: 1 << 51,
|
||||||
mode: CloneMode::Fail(libc::EXDEV),
|
mode: CloneMode::Fail(libc::EXDEV),
|
||||||
delay: Duration::ZERO,
|
delay: Duration::ZERO,
|
||||||
|
space_calls: AtomicU32::new(0),
|
||||||
|
space_fails_from: u32::MAX,
|
||||||
calls: AtomicU32::new(0),
|
calls: AtomicU32::new(0),
|
||||||
started: AtomicBool::new(false),
|
started: AtomicBool::new(false),
|
||||||
active: AtomicU32::new(0),
|
active: AtomicU32::new(0),
|
||||||
|
|
@ -784,6 +817,12 @@ mod tests {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Make the `n`th and later free-space reads fail, counting from 0.
|
||||||
|
fn space_fails_from(mut self, n: u32) -> Self {
|
||||||
|
self.space_fails_from = n;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn delay(mut self, delay: Duration) -> Self {
|
fn delay(mut self, delay: Duration) -> Self {
|
||||||
self.delay = delay;
|
self.delay = delay;
|
||||||
self
|
self
|
||||||
|
|
@ -792,6 +831,9 @@ mod tests {
|
||||||
|
|
||||||
impl Backend for Fake {
|
impl Backend for Fake {
|
||||||
fn space(&self, _path: &Path) -> io::Result<FsSpace> {
|
fn space(&self, _path: &Path) -> io::Result<FsSpace> {
|
||||||
|
if self.space_calls.fetch_add(1, Ordering::SeqCst) >= self.space_fails_from {
|
||||||
|
return Err(io::Error::from_raw_os_error(libc::EIO));
|
||||||
|
}
|
||||||
Ok(FsSpace {
|
Ok(FsSpace {
|
||||||
free: self.free,
|
free: self.free,
|
||||||
total: self.total,
|
total: self.total,
|
||||||
|
|
@ -1424,6 +1466,62 @@ mod tests {
|
||||||
assert_eq!(stats.cloned, 0);
|
assert_eq!(stats.cloned, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A salvage that fails after it created its directory removes that
|
||||||
|
/// directory and nothing else: an earlier salvage stays, the old root is
|
||||||
|
/// as it was, and no marker is written. Once without room for a copy, once
|
||||||
|
/// with a free-space read that fails.
|
||||||
|
#[test]
|
||||||
|
fn a_failed_salvage_removes_its_partial_directory() {
|
||||||
|
let body = "x".repeat(1 << 20);
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
"no room for a file",
|
||||||
|
Fake::new()
|
||||||
|
.space(CLONE_RESERVE + (512 << 10), 100 * GIB)
|
||||||
|
.mode(CloneMode::FirstOnly),
|
||||||
|
),
|
||||||
|
("free space unreadable", Fake::new().space_fails_from(1)),
|
||||||
|
];
|
||||||
|
for (what, fake) in cases {
|
||||||
|
let f = Fixture::with(fake);
|
||||||
|
f.put("home/iris/big", &body);
|
||||||
|
f.put("home/iris/small", "s");
|
||||||
|
// An earlier salvage of the same day, which must survive.
|
||||||
|
let earlier = f.salvage();
|
||||||
|
std::fs::create_dir_all(earlier.join("home")).unwrap();
|
||||||
|
std::fs::write(earlier.join("home/old"), "kept").unwrap();
|
||||||
|
|
||||||
|
assert!(f.try_rotate().is_err(), "{what}: must fail");
|
||||||
|
assert_eq!(
|
||||||
|
Fixture::names(&f.work),
|
||||||
|
[format!("salvaged-rootfs-{DATE}")],
|
||||||
|
"{what}: a partial directory is left"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::fs::read_to_string(earlier.join("home/old")).unwrap(),
|
||||||
|
"kept"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
f.root.join("home/iris/big").is_file(),
|
||||||
|
"{what}: root was touched"
|
||||||
|
);
|
||||||
|
assert!(f.root.join("home/iris/small").is_file(), "{what}");
|
||||||
|
assert!(!f.root.join("etc").join(MARKER).exists(), "{what}");
|
||||||
|
assert!(
|
||||||
|
Fixture::names(&f.top)
|
||||||
|
.iter()
|
||||||
|
.all(|n| !n.contains("pre-ephemeral")),
|
||||||
|
"{what}: the root was moved"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control: the same root with room and a working space read succeeds.
|
||||||
|
let ok = Fixture::with(Fake::new());
|
||||||
|
ok.put("home/iris/big", &body);
|
||||||
|
assert!(matches!(ok.rotate(), Outcome::Salvaged { .. }));
|
||||||
|
assert!(ok.salvage().join("home/iris/big").is_file());
|
||||||
|
}
|
||||||
|
|
||||||
/// A second request for a root that is being salvaged waits, then finds
|
/// A second request for a root that is being salvaged waits, then finds
|
||||||
/// the finished template: one salvage directory, one moved-aside root.
|
/// the finished template: one salvage directory, one moved-aside root.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,9 @@ pkgs.testers.runNixOSTest {
|
||||||
pkgs.hello
|
pkgs.hello
|
||||||
pkgs.tree
|
pkgs.tree
|
||||||
];
|
];
|
||||||
|
# The salvage skips when a copy would leave under 15% of the disk free, and
|
||||||
|
# this disk cannot clone files; the default 1 GiB leaves too little room.
|
||||||
|
virtualisation.diskSize = 4096;
|
||||||
# The agent bridge hive-priv names in the container's conf.
|
# The agent bridge hive-priv names in the container's conf.
|
||||||
networking.bridges.${bridge}.interfaces = [ ];
|
networking.bridges.${bridge}.interfaces = [ ];
|
||||||
networking.interfaces.${bridge}.ipv4.addresses = [
|
networking.interfaces.${bridge}.ipv4.addresses = [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue