deb,prune: clean up orphaned session lock files
Session locks live next to the session root, and failed or interrupted runs left them behind after the root was gone (seven stray locks on a used machine). Session removal now unlinks its own lock file, and prune collects the orphans: by default only the ones no live build holds (probed with a non-blocking flock), with --all every one of them. Locks guarding an existing session root are never touched.
This commit is contained in:
+85
-8
@@ -101,6 +101,9 @@ enum Artifact {
|
|||||||
Session(PathBuf),
|
Session(PathBuf),
|
||||||
/// A `pkh test` run directory (report artifacts).
|
/// A `pkh test` run directory (report artifacts).
|
||||||
TestRun(PathBuf),
|
TestRun(PathBuf),
|
||||||
|
/// An orphaned session lock file (`<slug>.lock` whose session root is
|
||||||
|
/// gone).
|
||||||
|
SessionLock(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Artifact {
|
impl Artifact {
|
||||||
@@ -112,7 +115,8 @@ impl Artifact {
|
|||||||
| Artifact::Tarball(p)
|
| Artifact::Tarball(p)
|
||||||
| Artifact::LogFile(p)
|
| Artifact::LogFile(p)
|
||||||
| Artifact::Session(p)
|
| Artifact::Session(p)
|
||||||
| Artifact::TestRun(p) => p,
|
| Artifact::TestRun(p)
|
||||||
|
| Artifact::SessionLock(p) => p,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +129,7 @@ impl Artifact {
|
|||||||
Artifact::LogFile(_) => "build log",
|
Artifact::LogFile(_) => "build log",
|
||||||
Artifact::Session(_) => "resumable build session",
|
Artifact::Session(_) => "resumable build session",
|
||||||
Artifact::TestRun(_) => "pkh test run",
|
Artifact::TestRun(_) => "pkh test run",
|
||||||
|
Artifact::SessionLock(_) => "stale session lockfile",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,17 +138,32 @@ impl Artifact {
|
|||||||
/// with `--all`, every session; otherwise only those untouched for longer
|
/// with `--all`, every session; otherwise only those untouched for longer
|
||||||
/// than the retention window ([`crate::deb::session::SESSION_RETENTION`])
|
/// than the retention window ([`crate::deb::session::SESSION_RETENTION`])
|
||||||
/// and the ones whose manifest is missing or unreadable (inert leftovers).
|
/// and the ones whose manifest is missing or unreadable (inert leftovers).
|
||||||
|
///
|
||||||
|
/// Orphaned lock files (`<slug>.lock` whose session root is gone — left
|
||||||
|
/// behind by failed/interrupted runs) are collected too: by default only
|
||||||
|
/// the ones no live build holds (probed with a non-blocking flock), with
|
||||||
|
/// `--all` every one of them.
|
||||||
fn discover_sessions(sessions_dir: &Path, all: bool) -> Vec<Artifact> {
|
fn discover_sessions(sessions_dir: &Path, all: bool) -> Vec<Artifact> {
|
||||||
let Ok(entries) = fs::read_dir(sessions_dir) else {
|
let Ok(entries) = fs::read_dir(sessions_dir) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
entries
|
let mut artifacts = Vec::new();
|
||||||
.flatten()
|
for entry in entries.flatten() {
|
||||||
.map(|entry| entry.path())
|
let path = entry.path();
|
||||||
.filter(|path| path.is_dir())
|
let name = entry.file_name().to_string_lossy().into_owned();
|
||||||
.filter(|path| is_prunable_session(path, all))
|
if let Some(slug) = name.strip_suffix(".lock") {
|
||||||
.map(Artifact::Session)
|
if !path.is_file() || sessions_dir.join(slug).is_dir() {
|
||||||
.collect()
|
continue;
|
||||||
|
}
|
||||||
|
let held = crate::deb::session::lock_is_held(&path).unwrap_or(true);
|
||||||
|
if all || !held {
|
||||||
|
artifacts.push(Artifact::SessionLock(path));
|
||||||
|
}
|
||||||
|
} else if path.is_dir() && is_prunable_session(&path, all) {
|
||||||
|
artifacts.push(Artifact::Session(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artifacts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discover the `pkh test` run directories prune may remove under
|
/// Discover the `pkh test` run directories prune may remove under
|
||||||
@@ -800,6 +820,7 @@ none /tmp/other proc rw 0 0
|
|||||||
Artifact::LogFile(_) => logs += 1,
|
Artifact::LogFile(_) => logs += 1,
|
||||||
Artifact::Session(_) => {}
|
Artifact::Session(_) => {}
|
||||||
Artifact::TestRun(_) => {}
|
Artifact::TestRun(_) => {}
|
||||||
|
Artifact::SessionLock(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(temp_dirs, 2);
|
assert_eq!(temp_dirs, 2);
|
||||||
@@ -1303,4 +1324,60 @@ none /tmp/other proc rw 0 0
|
|||||||
assert!(report.removed.contains(&fresh));
|
assert!(report.removed.contains(&fresh));
|
||||||
assert!(!fresh.exists());
|
assert!(!fresh.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Orphaned session lock files are pruned (unless held by a live
|
||||||
|
/// build); locks whose session root still exists are never touched.
|
||||||
|
#[test]
|
||||||
|
fn test_session_lock_pruning() {
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
let temp = tempdir().unwrap();
|
||||||
|
let sessions = temp.path().join("sessions");
|
||||||
|
let orphan_free = sessions.join("free.lock");
|
||||||
|
let orphan_held = sessions.join("held.lock");
|
||||||
|
let rooted = sessions.join("rooted.lock");
|
||||||
|
fs::create_dir_all(&sessions).unwrap();
|
||||||
|
fs::write(&orphan_free, b"").unwrap();
|
||||||
|
fs::write(&orphan_held, b"").unwrap();
|
||||||
|
fs::write(&rooted, b"").unwrap();
|
||||||
|
fs::create_dir_all(sessions.join("rooted")).unwrap();
|
||||||
|
|
||||||
|
// Hold one orphaned lock, as a live build would.
|
||||||
|
let holder = fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.open(&orphan_held)
|
||||||
|
.unwrap();
|
||||||
|
unsafe {
|
||||||
|
libc::flock(holder.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB);
|
||||||
|
}
|
||||||
|
|
||||||
|
let report = prune_in_roots(
|
||||||
|
temp.path(),
|
||||||
|
None,
|
||||||
|
Some(&sessions),
|
||||||
|
None,
|
||||||
|
PruneOptions::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(report.removed.contains(&orphan_free));
|
||||||
|
assert!(!orphan_free.exists());
|
||||||
|
assert!(orphan_held.exists(), "a held lock belongs to a live build");
|
||||||
|
assert!(rooted.exists(), "a lock guarding an existing root stays");
|
||||||
|
|
||||||
|
// --all takes the held orphan too (the root it guarded is gone).
|
||||||
|
drop(holder);
|
||||||
|
let report = prune_in_roots(
|
||||||
|
temp.path(),
|
||||||
|
None,
|
||||||
|
Some(&sessions),
|
||||||
|
None,
|
||||||
|
PruneOptions {
|
||||||
|
all: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(report.removed.contains(&orphan_held));
|
||||||
|
assert!(!orphan_held.exists());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user