deb: share the apt download cache across builds
With the chroot now per-run scratch, every build re-downloaded the packages its dependency install needed — the very cost the chroot reuse used to hide. Every chroot's /var/cache/apt/archives is now a bind mount over a shared cache at /var/tmp/pkh/apt-cache/<series>-<arch>/ (the chroot's architecture, like the tarball cache), which one run's installs fill and the next runs reuse: apt keeps downloaded packages through a dropped conf (APT::Keep-Downloaded-Packages), and an apt-get autoclean at the end of each run drops the debs the archive no longer serves. pkh prune removes caches untouched for a week. The run holds a host-side flock on the cache for its whole lifetime: apt's own archives lock is acquired inside the chroot's pid namespace, where a concurrent build's apt shows the holder as "process 0" — an invisible process it refuses to wait for — so pkh serializes the traffic itself; concurrent builds of the same target wait for each other. Any bind-mount failure falls back to the chroot's own cache with a warning (correct, just slower), and the temp-chroot teardown now unmounts everything under the chroot before rm -rf, so the removal can never reach through the cache bind mount and delete the shared cache itself.
This commit is contained in:
+102
-7
@@ -10,11 +10,17 @@
|
||||
//! bind-mounted `/proc` and overlay filesystems, so they require careful
|
||||
//! unmounting before they can be removed.
|
||||
//! - **Build records** under `/var/tmp/pkh/sessions/<slug>/<record-id>/`
|
||||
//! (see [`crate::deb::session`]): one immutable record per build; the
|
||||
//! (see [`crate::deb::session`]): one record per build (metadata and
|
||||
//! log for every run, the staged tree of `--incremental` ones); the
|
||||
//! default policy removes the ones untouched for longer than the
|
||||
//! record retention window plus the corrupt leftovers, `--all`
|
||||
//! removes every record (only the last five per target are kept by
|
||||
//! `pkh deb` itself).
|
||||
//! removes every record (at most five staged trees per target are
|
||||
//! kept by `pkh deb` itself).
|
||||
//! - **Shared apt download caches** under `/var/tmp/pkh/apt-cache/`
|
||||
//! (see [`crate::deb::ephemeral::apt_cache_dir`]): the default policy
|
||||
//! removes the ones untouched for longer than the retention window,
|
||||
//! `--all` removes them all; the `.debs` they hold are cheap to
|
||||
//! re-download.
|
||||
//! - **Cached chroot tarballs** (`~/.cache/pkh/*-buildd.tar.xz`) and their
|
||||
//! **stale download lockfiles** (`~/.cache/pkh/*.lock` untouched for longer
|
||||
//! than [`LOCK_STALE_AFTER`]; younger lockfiles may belong to a concurrent
|
||||
@@ -103,6 +109,9 @@ enum Artifact {
|
||||
Session(PathBuf),
|
||||
/// A `pkh test` run directory (report artifacts).
|
||||
TestRun(PathBuf),
|
||||
/// A shared apt download cache of one target (see
|
||||
/// [`crate::deb::ephemeral::apt_cache_dir`]).
|
||||
AptCache(PathBuf),
|
||||
/// An orphaned record lock file (`<record id>.lock` whose record
|
||||
/// root is gone).
|
||||
SessionLock(PathBuf),
|
||||
@@ -118,6 +127,7 @@ impl Artifact {
|
||||
| Artifact::LogFile(p)
|
||||
| Artifact::Session(p)
|
||||
| Artifact::TestRun(p)
|
||||
| Artifact::AptCache(p)
|
||||
| Artifact::SessionLock(p) => p,
|
||||
}
|
||||
}
|
||||
@@ -131,6 +141,7 @@ impl Artifact {
|
||||
Artifact::LogFile(_) => "build log",
|
||||
Artifact::Session(_) => "build record",
|
||||
Artifact::TestRun(_) => "pkh test run",
|
||||
Artifact::AptCache(_) => "shared apt download cache",
|
||||
Artifact::SessionLock(_) => "stale record lockfile",
|
||||
}
|
||||
}
|
||||
@@ -231,6 +242,25 @@ fn discover_test_runs(tests_dir: &Path, all: bool) -> Vec<Artifact> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Discover the shared apt download caches prune may remove under
|
||||
/// `apt_cache_dir` (`/var/tmp/pkh/apt-cache`, one `<series>-<arch>`
|
||||
/// subdirectory per target): with `--all`, every one; otherwise only
|
||||
/// those untouched for longer than the retention window. A cache in use
|
||||
/// by a live build is fresh by construction — its `.debs` are being
|
||||
/// read and written.
|
||||
fn discover_apt_cache(apt_cache_dir: &Path, all: bool) -> Vec<Artifact> {
|
||||
let Ok(entries) = fs::read_dir(apt_cache_dir) else {
|
||||
return Vec::new();
|
||||
};
|
||||
entries
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir())
|
||||
.filter(|path| is_stale_path(path, all))
|
||||
.map(Artifact::AptCache)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Whether a record root is prunable under the given policy: everything
|
||||
/// with `--all`, otherwise stale (untouched beyond the retention window)
|
||||
/// or corrupt (no readable manifest) records. Live records record a fresh
|
||||
@@ -582,19 +612,21 @@ pub fn prune_in(
|
||||
cache_dir: Option<&Path>,
|
||||
options: PruneOptions,
|
||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
prune_in_roots(temp_dir, cache_dir, None, None, options)
|
||||
prune_in_roots(temp_dir, cache_dir, None, None, None, options)
|
||||
}
|
||||
|
||||
/// Execute the prune operation against explicit roots.
|
||||
///
|
||||
/// `sessions_dir` opts in to build-session pruning and `tests_dir` to
|
||||
/// test-run pruning (the production [`prune()`] passes the real roots;
|
||||
/// tests pass controlled directories or `None`).
|
||||
/// `sessions_dir` opts in to build-record pruning, `tests_dir` to
|
||||
/// test-run pruning and `apt_cache_dir` to shared apt-cache pruning (the
|
||||
/// production [`prune()`] passes the real roots; tests pass controlled
|
||||
/// directories or `None`).
|
||||
pub fn prune_in_roots(
|
||||
temp_dir: &Path,
|
||||
cache_dir: Option<&Path>,
|
||||
sessions_dir: Option<&Path>,
|
||||
tests_dir: Option<&Path>,
|
||||
apt_cache_dir: Option<&Path>,
|
||||
options: PruneOptions,
|
||||
) -> Result<PruneReport, Box<dyn std::error::Error>> {
|
||||
let mut artifacts = discover_artifacts(temp_dir, cache_dir);
|
||||
@@ -604,6 +636,9 @@ pub fn prune_in_roots(
|
||||
if let Some(tests_dir) = tests_dir {
|
||||
artifacts.extend(discover_test_runs(tests_dir, options.all));
|
||||
}
|
||||
if let Some(apt_cache_dir) = apt_cache_dir {
|
||||
artifacts.extend(discover_apt_cache(apt_cache_dir, options.all));
|
||||
}
|
||||
|
||||
let mut report = PruneReport {
|
||||
dry_run: options.dry_run,
|
||||
@@ -671,6 +706,7 @@ pub fn prune(options: PruneOptions) -> Result<PruneReport, Box<dyn std::error::E
|
||||
cache.as_deref(),
|
||||
Some(&crate::deb::session::sessions_root()),
|
||||
Some(&crate::test::runs_root()),
|
||||
Some(&PathBuf::from(crate::deb::ephemeral::APT_CACHE_ROOT)),
|
||||
options,
|
||||
)
|
||||
}
|
||||
@@ -872,6 +908,7 @@ none /tmp/other proc rw 0 0
|
||||
Artifact::LogFile(_) => logs += 1,
|
||||
Artifact::Session(_) => {}
|
||||
Artifact::TestRun(_) => {}
|
||||
Artifact::AptCache(_) => {}
|
||||
Artifact::SessionLock(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -1307,6 +1344,7 @@ none /tmp/other proc rw 0 0
|
||||
None,
|
||||
Some(&sessions),
|
||||
None,
|
||||
None,
|
||||
PruneOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1322,6 +1360,7 @@ none /tmp/other proc rw 0 0
|
||||
None,
|
||||
Some(&sessions),
|
||||
None,
|
||||
None,
|
||||
PruneOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
@@ -1356,6 +1395,7 @@ none /tmp/other proc rw 0 0
|
||||
None,
|
||||
None,
|
||||
Some(&tests),
|
||||
None,
|
||||
PruneOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1369,6 +1409,59 @@ none /tmp/other proc rw 0 0
|
||||
None,
|
||||
None,
|
||||
Some(&tests),
|
||||
None,
|
||||
PruneOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(report.removed.contains(&fresh));
|
||||
assert!(!fresh.exists());
|
||||
}
|
||||
|
||||
/// The shared apt download caches follow the retention window: a
|
||||
/// cache untouched for over a week goes, one in recent use stays,
|
||||
/// `--all` takes everything.
|
||||
#[test]
|
||||
fn prunes_stale_apt_caches_by_retention() {
|
||||
let temp = tempdir().unwrap();
|
||||
let apt_cache = temp.path().join("apt-cache");
|
||||
let stale = apt_cache.join("noble-amd64");
|
||||
let fresh = apt_cache.join("sid-riscv64");
|
||||
for dir in [&stale, &fresh] {
|
||||
fs::create_dir_all(dir.join("partial")).unwrap();
|
||||
fs::write(dir.join("some-deb_1.0_amd64.deb"), "deb").unwrap();
|
||||
}
|
||||
// Directories open read-only (write-open is a file-only mode):
|
||||
// futimens still applies through the read handle.
|
||||
let dir = fs::File::open(&stale).unwrap();
|
||||
dir.set_times(
|
||||
std::fs::FileTimes::new()
|
||||
.set_modified(SystemTime::now() - std::time::Duration::from_secs(8 * 24 * 60 * 60)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let report = prune_in_roots(
|
||||
temp.path(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&apt_cache),
|
||||
PruneOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(report.removed.contains(&stale));
|
||||
assert!(!stale.exists());
|
||||
assert!(fresh.exists(), "a recently used cache is not pruned");
|
||||
|
||||
// --all removes every cache.
|
||||
let report = prune_in_roots(
|
||||
temp.path(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&apt_cache),
|
||||
PruneOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
@@ -1415,6 +1508,7 @@ none /tmp/other proc rw 0 0
|
||||
None,
|
||||
Some(&sessions),
|
||||
None,
|
||||
None,
|
||||
PruneOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -1432,6 +1526,7 @@ none /tmp/other proc rw 0 0
|
||||
None,
|
||||
Some(&sessions),
|
||||
None,
|
||||
None,
|
||||
PruneOptions {
|
||||
all: true,
|
||||
..Default::default()
|
||||
|
||||
Reference in New Issue
Block a user