deb,config: keep the newest and the newest successful staged trees
The retention kept the newest five staged trees per target, so the staged trees of a kernel-scale target alone could hold three-digit GiB. The policy is now the pair that earns its disk: the newest staged tree (what a failed run is re-forked from explicitly) and the newest successful one (what the next --incremental run forks automatically) — one record when they coincide; every other staged tree degrades to metadata-only at the next build's retention. The retention decision moved into a pure planner over (success, size) so the policy is table-testable. The size cap no longer drops a tree outright: an --incremental run's own tree is always kept by its own run — the user asked for a carried tree, and at least one always exists — and the cap only thins the kept pair while another member covers the resume, the newest successful tree being the last one standing. Oversized trees left by older pkh versions or interrupted builds come down at the next build's retention, once their lock is free. The cap moves to the first user settings file, config.yml in ~/.config/pkh (YAML, like the data files; missing file or keys mean defaults, a corrupt file is backed up, PKH_STAGED_MAXSIZE overrides), replacing the JSON sketch from the previous commit. Default 20G, 0 disables the size rule. Every retained tree's size is measured once at retention and recorded in the manifest, so sweeps never re-walk the trees.
This commit is contained in:
@@ -164,9 +164,9 @@ pkh deb # plain run: fresh chroot, fresh tree; the
|
|||||||
# (nothing is kept even on failure or
|
# (nothing is kept even on failure or
|
||||||
# interrupt)
|
# interrupt)
|
||||||
pkh deb --incremental # fork the newest successful record of the
|
pkh deb --incremental # fork the newest successful record of the
|
||||||
# target (among the last five carrying a staged
|
# target (pkh keeps the newest and the newest
|
||||||
# tree) and keep this run's staged tree, for
|
# successful staged trees) and keep this run's
|
||||||
# the next fork to carry
|
# staged tree, for the next fork to carry
|
||||||
pkh deb --incremental <id>
|
pkh deb --incremental <id>
|
||||||
# fork that exact record (see `pkh deb list`);
|
# fork that exact record (see `pkh deb list`);
|
||||||
# a failed record is fine if its staged tree is
|
# a failed record is fine if its staged tree is
|
||||||
@@ -183,18 +183,24 @@ pkh deb profile [<id>] # a record's per-phase timing profile: how
|
|||||||
pkh deb --no-ccache # build without the shared compile cache
|
pkh deb --no-ccache # build without the shared compile cache
|
||||||
```
|
```
|
||||||
|
|
||||||
Each target keeps at most five staged trees: when a sixth is retained,
|
Each target keeps two staged trees: the newest one and the newest
|
||||||
the oldest degrades to a metadata-only record (manifest and log stay).
|
successful one — the tree the next `--incremental` run forks
|
||||||
A staged tree larger than 20 GiB is never kept at all — a tree that
|
automatically, and the tree a failed run can be re-forked from
|
||||||
size costs more disk than the resume it buys, so its record degrades
|
explicitly. An `--incremental` run always keeps its own tree, so at
|
||||||
too and the next build re-stages from scratch. The cap is a setting:
|
least one staged tree exists even when the newest build failed. Every
|
||||||
|
other staged tree degrades to a metadata-only record (manifest and log
|
||||||
|
stay).
|
||||||
|
|
||||||
```json
|
A staged tree larger than 20 GiB comes down as well, as soon as the
|
||||||
// ~/.config/pkh/config.json (missing = defaults; keys may be omitted)
|
pair has another member covering the resume — a tree that size costs
|
||||||
{ "staged_maxsize": "20G" }
|
more disk than the resume it buys. The cap is a setting:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ~/.config/pkh/config.yml (missing file or keys = defaults)
|
||||||
|
staged_maxsize: 20G
|
||||||
```
|
```
|
||||||
|
|
||||||
`"0"` disables the size rule entirely, and `PKH_STAGED_MAXSIZE`
|
`0` disables the size rule entirely, and `PKH_STAGED_MAXSIZE`
|
||||||
overrides the file when set. `pkh prune` removes records older than a
|
overrides the file when set. `pkh prune` removes records older than a
|
||||||
week. `pkh prune --dry-run` previews everything it would remove,
|
week. `pkh prune --dry-run` previews everything it would remove,
|
||||||
grouped by kind with sizes; `--all` ignores the age and removes
|
grouped by kind with sizes; `--all` ignores the age and removes
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
//! User settings: the optional `config.json` in the pkh configuration
|
//! User settings: the optional `config.yml` in the pkh configuration
|
||||||
//! directory (the same `~/.config/pkh/` that holds `contexts.json`),
|
//! directory (the same `~/.config/pkh/` that holds `contexts.json`),
|
||||||
//! its defaults, and the environment overrides.
|
//! its defaults, and the environment overrides.
|
||||||
//!
|
//!
|
||||||
@@ -21,10 +21,10 @@ pub fn config_dir() -> Option<PathBuf> {
|
|||||||
|
|
||||||
/// The settings file in [`config_dir`].
|
/// The settings file in [`config_dir`].
|
||||||
fn config_path() -> Option<PathBuf> {
|
fn config_path() -> Option<PathBuf> {
|
||||||
config_dir().map(|dir| dir.join("config.json"))
|
config_dir().map(|dir| dir.join("config.yml"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The user-adjustable settings, as stored in `config.json`. New fields
|
/// The user-adjustable settings, as stored in `config.yml`. New fields
|
||||||
/// must default — the file always may carry a subset.
|
/// must default — the file always may carry a subset.
|
||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -61,7 +61,7 @@ fn file_staged_maxsize() -> u64 {
|
|||||||
let Ok(text) = std::fs::read_to_string(&path) else {
|
let Ok(text) = std::fs::read_to_string(&path) else {
|
||||||
return DEFAULT;
|
return DEFAULT;
|
||||||
};
|
};
|
||||||
let settings: Settings = match serde_json::from_str(&text) {
|
let settings: Settings = match serde_yaml::from_str(&text) {
|
||||||
Ok(settings) => settings,
|
Ok(settings) => settings,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!(
|
log::error!(
|
||||||
|
|||||||
+4
-4
@@ -258,10 +258,10 @@ pub enum Incremental {
|
|||||||
#[default]
|
#[default]
|
||||||
Off,
|
Off,
|
||||||
/// `--incremental`: fork the auto-selected base — the newest
|
/// `--incremental`: fork the auto-selected base — the newest
|
||||||
/// successful record of this identity among the last five carrying
|
/// successful record of this identity carrying a staged tree — and
|
||||||
/// a staged tree — and retain the new record's staged tree. With no
|
/// retain the new record's staged tree. With no candidate the build
|
||||||
/// candidate the build runs from scratch and its record becomes
|
/// runs from scratch and its record becomes the identity's first
|
||||||
/// the identity's first base.
|
/// base.
|
||||||
Auto,
|
Auto,
|
||||||
/// `--incremental <id>`: fork that exact record (unique id or
|
/// `--incremental <id>`: fork that exact record (unique id or
|
||||||
/// prefix, restricted to the records of this host tree; a FAILED
|
/// prefix, restricted to the records of this host tree; a FAILED
|
||||||
|
|||||||
+332
-148
@@ -64,11 +64,14 @@ pub(crate) const SCHEMA_VERSION: u32 = 1;
|
|||||||
/// removes it.
|
/// removes it.
|
||||||
pub const SESSION_RETENTION: Duration = Duration::from_secs(7 * 24 * 60 * 60);
|
pub const SESSION_RETENTION: Duration = Duration::from_secs(7 * 24 * 60 * 60);
|
||||||
|
|
||||||
/// How many records of one identity may retain their tree-upper at the
|
/// How many staged trees one identity retains: the newest one and the
|
||||||
/// same time: when a newer upper-retaining record appears, the oldest
|
/// newest successful one — a single record when they coincide. Every
|
||||||
/// one beyond this count degrades to a metadata-only record (its
|
/// other upper-retaining record degrades to metadata-only (its upperdir
|
||||||
/// upperdir and host-tree snapshot are deleted; manifest and log stay).
|
/// and host-tree snapshot are deleted; manifest and log stay): the
|
||||||
pub(crate) const KEEP_UPPERS: usize = 5;
|
/// newest successful tree is what the next `--incremental` run forks
|
||||||
|
/// automatically, the newest tree is what a failed run can be re-forked
|
||||||
|
/// from explicitly, and anything older earns no disk.
|
||||||
|
pub(crate) const KEEP_UPPERS: usize = 2;
|
||||||
|
|
||||||
/// The build is running (`last_outcome`).
|
/// The build is running (`last_outcome`).
|
||||||
pub const OUTCOME_RUNNING: &str = "running";
|
pub const OUTCOME_RUNNING: &str = "running";
|
||||||
@@ -291,15 +294,16 @@ pub struct Session {
|
|||||||
/// What the end-of-run retention of a record's staged tree does,
|
/// What the end-of-run retention of a record's staged tree does,
|
||||||
/// decided from the request and the disk: a keep requested for a run
|
/// decided from the request and the disk: a keep requested for a run
|
||||||
/// that ended before staging created a tree-upper has nothing to keep,
|
/// that ended before staging created a tree-upper has nothing to keep,
|
||||||
/// and the record must not claim otherwise. A keep of a tree over the
|
/// and the record must not claim otherwise. The size cap does not
|
||||||
/// retention size cap also drops it: a staged tree only earns its disk
|
/// decide here — the build's own tree is always kept by its own run
|
||||||
/// by making the next build resume, and a tree too large to keep is too
|
/// (an `--incremental` user always has at least one staged tree); the
|
||||||
/// large to keep five of.
|
/// retention sweep applies the cap afterwards, to records whose lock is
|
||||||
|
/// free ([`Session::enforce_upper_cap`]).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum UpperRetention {
|
pub(crate) enum UpperRetention {
|
||||||
/// Keep the staged tree: the record is a fork candidate.
|
/// Keep the staged tree: the record is a fork candidate.
|
||||||
Keep,
|
Keep,
|
||||||
/// Drop the staged tree: a plain run, or a tree over the cap.
|
/// Drop the staged tree: a plain run.
|
||||||
Drop,
|
Drop,
|
||||||
/// An `--incremental` run whose staging never happened: metadata
|
/// An `--incremental` run whose staging never happened: metadata
|
||||||
/// and log only, `kept_upper` stays false.
|
/// and log only, `kept_upper` stays false.
|
||||||
@@ -308,23 +312,81 @@ pub(crate) enum UpperRetention {
|
|||||||
|
|
||||||
/// Decide the retention of a record's staged tree: an
|
/// Decide the retention of a record's staged tree: an
|
||||||
/// `--incremental` run keeps it — unless the run ended before staging
|
/// `--incremental` run keeps it — unless the run ended before staging
|
||||||
/// created one (`upper_exists` false), or the tree's measured
|
/// created one, in which case there is nothing to keep — and a plain
|
||||||
/// `upper_bytes` exceed `max_bytes` (0 = unlimited), in which case
|
/// run drops it.
|
||||||
/// there is nothing worth keeping — and a plain run drops it.
|
pub(crate) fn upper_retention(requested: bool, upper_exists: bool) -> UpperRetention {
|
||||||
pub(crate) fn upper_retention(
|
|
||||||
requested: bool,
|
|
||||||
upper_exists: bool,
|
|
||||||
upper_bytes: u64,
|
|
||||||
max_bytes: u64,
|
|
||||||
) -> UpperRetention {
|
|
||||||
match (requested, upper_exists) {
|
match (requested, upper_exists) {
|
||||||
|
(true, true) => UpperRetention::Keep,
|
||||||
(true, false) => UpperRetention::KeepNothing,
|
(true, false) => UpperRetention::KeepNothing,
|
||||||
(false, _) => UpperRetention::Drop,
|
(false, _) => UpperRetention::Drop,
|
||||||
(true, true) if max_bytes != 0 && upper_bytes > max_bytes => UpperRetention::Drop,
|
|
||||||
(true, true) => UpperRetention::Keep,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why the retention plan keeps or degrades a staged tree.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum UpperDecision {
|
||||||
|
/// The newest staged tree, or the newest successful one: kept.
|
||||||
|
Keep,
|
||||||
|
/// Neither the newest staged tree nor the newest successful one.
|
||||||
|
DropCount,
|
||||||
|
/// Part of the retained pair but over the size cap, with the
|
||||||
|
/// pair's other member still covering the incremental resume.
|
||||||
|
DropSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One upper-retaining record as the retention planner sees it,
|
||||||
|
/// newest first.
|
||||||
|
pub(crate) struct UpperPlanRecord {
|
||||||
|
/// Whether the record's build succeeded — the newest successful
|
||||||
|
/// tree is what the next `--incremental` run forks automatically.
|
||||||
|
pub success: bool,
|
||||||
|
/// The tree's measured size in bytes.
|
||||||
|
pub bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The retention plan for upper-retaining records (newest-first
|
||||||
|
/// input): keep the newest staged tree and the newest successful one,
|
||||||
|
/// degrade everything else, and apply the size cap to the pair without
|
||||||
|
/// ever emptying it — an `--incremental` user always has at least one
|
||||||
|
/// staged tree. When the cap forces a choice inside the pair, the
|
||||||
|
/// failed newest tree yields first: the successful tree is the one the
|
||||||
|
/// next run forks automatically.
|
||||||
|
pub(crate) fn upper_retention_plan(
|
||||||
|
records: &[UpperPlanRecord],
|
||||||
|
max_bytes: u64,
|
||||||
|
) -> Vec<UpperDecision> {
|
||||||
|
let mut decisions = vec![UpperDecision::DropCount; records.len()];
|
||||||
|
if records.is_empty() {
|
||||||
|
return decisions;
|
||||||
|
}
|
||||||
|
let mut keep: Vec<usize> = vec![0];
|
||||||
|
if let Some(success_idx) = records.iter().position(|record| record.success)
|
||||||
|
&& success_idx != 0
|
||||||
|
{
|
||||||
|
keep.push(success_idx);
|
||||||
|
}
|
||||||
|
if max_bytes != 0 {
|
||||||
|
// Try the cap on the pair, least valuable first: the failed
|
||||||
|
// newest tree, then the successful one — and never the last
|
||||||
|
// tree standing.
|
||||||
|
let mut drop_order: Vec<usize> = keep.clone();
|
||||||
|
drop_order.sort_by_key(|&idx| records[idx].success as usize);
|
||||||
|
for idx in drop_order {
|
||||||
|
if keep.len() <= 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if records[idx].bytes > max_bytes {
|
||||||
|
keep.retain(|&kept| kept != idx);
|
||||||
|
decisions[idx] = UpperDecision::DropSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for &idx in &keep {
|
||||||
|
decisions[idx] = UpperDecision::Keep;
|
||||||
|
}
|
||||||
|
decisions
|
||||||
|
}
|
||||||
|
|
||||||
/// The staged-tree retention cap in bytes (`0` = unlimited), from the
|
/// The staged-tree retention cap in bytes (`0` = unlimited), from the
|
||||||
/// user settings (see [`crate::config`]).
|
/// user settings (see [`crate::config`]).
|
||||||
pub(crate) fn staged_maxsize() -> u64 {
|
pub(crate) fn staged_maxsize() -> u64 {
|
||||||
@@ -486,12 +548,14 @@ impl Session {
|
|||||||
/// Retain the record at the end of its build: the outcome is
|
/// Retain the record at the end of its build: the outcome is
|
||||||
/// recorded on success (failures already carry theirs), and the staged
|
/// recorded on success (failures already carry theirs), and the staged
|
||||||
/// tree follows [`upper_retention`] — kept exactly for `--incremental`
|
/// tree follows [`upper_retention`] — kept exactly for `--incremental`
|
||||||
/// runs that actually staged one under the retention size cap; a
|
/// runs that actually staged one, whatever its size (an `--incremental`
|
||||||
/// plain run's tree is dropped, a run that ended before staging keeps
|
/// user always has at least one staged tree); a plain run's tree is
|
||||||
/// nothing and a tree over the cap is dropped, all leaving a
|
/// dropped and a run that ended before staging keeps nothing, both
|
||||||
/// metadata-only record (manifest + log). The chroot was already
|
/// leaving a metadata-only record (manifest + log). The retention
|
||||||
/// removed by the caller's teardown. The interrupt path is handled by
|
/// sweep afterwards applies the count and size policies to the
|
||||||
/// the SIGINT cleanup hook, which follows the same policy.
|
/// identity's older trees. The chroot was already removed by the
|
||||||
|
/// caller's teardown. The interrupt path is handled by the SIGINT
|
||||||
|
/// cleanup hook, which follows the same policy.
|
||||||
pub(crate) fn retain_upper(&self, keep_upper: bool, succeeded: bool) {
|
pub(crate) fn retain_upper(&self, keep_upper: bool, succeeded: bool) {
|
||||||
self.retain_upper_with_cap(keep_upper, succeeded, staged_maxsize());
|
self.retain_upper_with_cap(keep_upper, succeeded, staged_maxsize());
|
||||||
}
|
}
|
||||||
@@ -505,12 +569,7 @@ impl Session {
|
|||||||
let id = self.manifest().id;
|
let id = self.manifest().id;
|
||||||
let upper_bytes = (keep_upper && self.has_upper())
|
let upper_bytes = (keep_upper && self.has_upper())
|
||||||
.then(|| crate::utils::size::tree_sizes(&[self.upper_dir().as_path()])[0]);
|
.then(|| crate::utils::size::tree_sizes(&[self.upper_dir().as_path()])[0]);
|
||||||
match upper_retention(
|
match upper_retention(keep_upper, self.has_upper()) {
|
||||||
keep_upper,
|
|
||||||
self.has_upper(),
|
|
||||||
upper_bytes.unwrap_or(0),
|
|
||||||
max_bytes,
|
|
||||||
) {
|
|
||||||
UpperRetention::Keep => {
|
UpperRetention::Keep => {
|
||||||
self.with_manifest(|manifest| {
|
self.with_manifest(|manifest| {
|
||||||
manifest.kept_upper = true;
|
manifest.kept_upper = true;
|
||||||
@@ -530,17 +589,7 @@ impl Session {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
UpperRetention::Drop => {
|
UpperRetention::Drop => {
|
||||||
if upper_bytes.is_some() {
|
log::debug!("Dropping the staged tree of build record {id} (plain run)");
|
||||||
log::info!(
|
|
||||||
"Build record {id} dropped its staged tree ({}, over \
|
|
||||||
the retention cap of {}; the next build re-stages \
|
|
||||||
from scratch)",
|
|
||||||
crate::utils::size::format_bytes(upper_bytes.unwrap_or(0)),
|
|
||||||
crate::utils::size::format_bytes(max_bytes)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
log::debug!("Dropping the staged tree of build record {id} (plain run)");
|
|
||||||
}
|
|
||||||
self.wipe_upper();
|
self.wipe_upper();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,15 +612,17 @@ impl Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enforce the retention caps for this record's identity (the size
|
/// Enforce the retention policy for this record's identity: keep the
|
||||||
/// cap comes from the user settings, [`staged_maxsize`]): degrade to
|
/// newest staged tree and the newest successful one
|
||||||
/// metadata-only (upperdir and snapshot deleted,
|
/// ([`KEEP_UPPERS`]), degrade every other upper-retaining record to
|
||||||
/// manifest and log kept) the oldest upper-retaining records beyond
|
/// metadata-only (upperdir and snapshot deleted, manifest and log
|
||||||
/// [`KEEP_UPPERS`], and every upper-retaining record whose staged
|
/// kept), and apply the size cap to the kept pair without ever
|
||||||
/// tree exceeds `max_bytes` — the newest build's own tree included,
|
/// emptying it — covering trees kept oversized by older pkh versions
|
||||||
/// covering trees kept oversized by older pkh versions or
|
/// or interrupted builds too. The record whose build just finished
|
||||||
/// interrupted builds. Records locked by a live build are never
|
/// (and any record locked by a live build) is never degraded: an
|
||||||
/// degraded.
|
/// `--incremental` run's own tree is the at-least-one tree the user
|
||||||
|
/// asked for, and the sweep of a later build applies the cap to it
|
||||||
|
/// once its lock is free.
|
||||||
fn enforce_upper_cap(&self, max_bytes: u64) {
|
fn enforce_upper_cap(&self, max_bytes: u64) {
|
||||||
// The record root is <sessions root>/<slug>/<id>: the sessions
|
// The record root is <sessions root>/<slug>/<id>: the sessions
|
||||||
// root is two levels up, keeping the cap on the same root the
|
// root is two levels up, keeping the cap on the same root the
|
||||||
@@ -585,38 +636,54 @@ impl Session {
|
|||||||
.filter(|record| record.has_upper())
|
.filter(|record| record.has_upper())
|
||||||
.collect();
|
.collect();
|
||||||
uppers.sort_by(|a, b| record_ordering(a, b).reverse());
|
uppers.sort_by(|a, b| record_ordering(a, b).reverse());
|
||||||
for (skip, record) in uppers.into_iter().enumerate() {
|
let plan_records: Vec<UpperPlanRecord> = uppers
|
||||||
// The size only decides for records the count cap would keep:
|
.iter()
|
||||||
// beyond KEEP_UPPERS the record degrades regardless, and
|
.map(|record| UpperPlanRecord {
|
||||||
// measuring its tree would be a wasted walk.
|
success: record.manifest().last_outcome == OUTCOME_SUCCESS,
|
||||||
let over_cap = skip < KEEP_UPPERS && max_bytes != 0 && upper_size(&record) > max_bytes;
|
bytes: upper_size(record),
|
||||||
if skip < KEEP_UPPERS && !over_cap {
|
})
|
||||||
continue;
|
.collect();
|
||||||
|
for (record, decision) in uppers
|
||||||
|
.into_iter()
|
||||||
|
.zip(upper_retention_plan(&plan_records, max_bytes))
|
||||||
|
{
|
||||||
|
match decision {
|
||||||
|
UpperDecision::Keep => {}
|
||||||
|
UpperDecision::DropCount | UpperDecision::DropSize => {
|
||||||
|
if record.root == self.root || lock_is_held(&record.lock_path()).unwrap_or(true)
|
||||||
|
{
|
||||||
|
log::debug!(
|
||||||
|
"Record {} is locked by a live build; skipping \
|
||||||
|
the retention sweep",
|
||||||
|
record.manifest().id
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match decision {
|
||||||
|
UpperDecision::DropSize => {
|
||||||
|
log::info!(
|
||||||
|
"Record {} degrades to metadata-only (its \
|
||||||
|
staged tree is {}, over the retention cap \
|
||||||
|
of {})",
|
||||||
|
record.manifest().id,
|
||||||
|
crate::utils::size::format_bytes(
|
||||||
|
record.manifest().staged_bytes.unwrap_or(0)
|
||||||
|
),
|
||||||
|
crate::utils::size::format_bytes(max_bytes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::debug!(
|
||||||
|
"Record {} degrades to metadata-only (neither \
|
||||||
|
the newest staged tree of {identity} nor the \
|
||||||
|
newest successful one)",
|
||||||
|
record.manifest().id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
record.wipe_upper();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if record.root == self.root || lock_is_held(&record.lock_path()).unwrap_or(true) {
|
|
||||||
log::debug!(
|
|
||||||
"Record {} is locked by a live build; skipping the \
|
|
||||||
upper cap",
|
|
||||||
record.manifest().id
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if over_cap {
|
|
||||||
log::info!(
|
|
||||||
"Record {} degrades to metadata-only (its staged tree \
|
|
||||||
is {}, over the retention cap of {})",
|
|
||||||
record.manifest().id,
|
|
||||||
crate::utils::size::format_bytes(record.manifest().staged_bytes.unwrap_or(0)),
|
|
||||||
crate::utils::size::format_bytes(max_bytes)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
log::debug!(
|
|
||||||
"Record {} degrades to metadata-only (beyond the newest \
|
|
||||||
{KEEP_UPPERS} staged trees of {identity})",
|
|
||||||
record.manifest().id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
record.wipe_upper();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,18 +716,10 @@ impl Session {
|
|||||||
/// the interrupted thread).
|
/// the interrupted thread).
|
||||||
pub(crate) fn record_interrupted(&self, keep_upper: bool) {
|
pub(crate) fn record_interrupted(&self, keep_upper: bool) {
|
||||||
self.set_outcome(OUTCOME_FAILED);
|
self.set_outcome(OUTCOME_FAILED);
|
||||||
let bytes = (keep_upper && self.has_upper())
|
// An interrupted tree's size is unknown until the next sweep
|
||||||
.then(|| crate::utils::size::tree_sizes(&[self.upper_dir().as_path()])[0]);
|
// measures it: record the claim, not a walk, on the interrupted
|
||||||
if upper_retention(
|
// thread.
|
||||||
keep_upper,
|
if upper_retention(keep_upper, self.has_upper()) == UpperRetention::Keep {
|
||||||
self.has_upper(),
|
|
||||||
bytes.unwrap_or(0),
|
|
||||||
staged_maxsize(),
|
|
||||||
) == UpperRetention::Keep
|
|
||||||
{
|
|
||||||
if let Some(bytes) = bytes {
|
|
||||||
self.set_staged_bytes(bytes);
|
|
||||||
}
|
|
||||||
self.with_manifest(|manifest| {
|
self.with_manifest(|manifest| {
|
||||||
manifest.kept_upper = true;
|
manifest.kept_upper = true;
|
||||||
});
|
});
|
||||||
@@ -3166,24 +3225,38 @@ mod tests {
|
|||||||
assert_eq!(load_all_in(root.path()).len(), 2);
|
assert_eq!(load_all_in(root.path()).len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retaining a sixth staged tree degrades the oldest one to a
|
/// The retention policy: a newer staged tree degrades every older
|
||||||
/// metadata-only record (upperdir and snapshot deleted, manifest and
|
/// one except the newest successful one — the pair the target keeps
|
||||||
/// log kept, `kept_upper` flipped): at most five staged trees per
|
/// (newest + newest-successful) — to metadata-only records (upperdir
|
||||||
/// target at any time.
|
/// and snapshot deleted, manifest and log kept, `kept_upper`
|
||||||
|
/// flipped).
|
||||||
#[test]
|
#[test]
|
||||||
fn retaining_a_sixth_upper_degrades_the_oldest() {
|
fn retaining_a_newer_upper_degrades_everything_but_the_pair() {
|
||||||
let (root, tarball) = sessions_fixture();
|
let (root, tarball) = sessions_fixture();
|
||||||
let identity = test_identity();
|
let identity = test_identity();
|
||||||
let host_tree = root.path().join("tree");
|
let host_tree = root.path().join("tree");
|
||||||
|
// Four running records, then a successful one, then a failed
|
||||||
|
// newest: the pair is the failed newest (the explicit-fork base)
|
||||||
|
// and the successful one before it (the auto-fork base).
|
||||||
let mut oldest = None;
|
let mut oldest = None;
|
||||||
for i in 0..6 {
|
for (i, outcome) in [
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(OUTCOME_SUCCESS),
|
||||||
|
Some(OUTCOME_FAILED),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
let record = record_with_upper(
|
let record = record_with_upper(
|
||||||
root.path(),
|
root.path(),
|
||||||
&tarball,
|
&tarball,
|
||||||
&host_tree,
|
&host_tree,
|
||||||
&identity,
|
&identity,
|
||||||
&format!("1.{i}"),
|
&format!("1.{i}"),
|
||||||
None,
|
outcome,
|
||||||
);
|
);
|
||||||
oldest.get_or_insert_with(|| record.manifest().id.clone());
|
oldest.get_or_insert_with(|| record.manifest().id.clone());
|
||||||
drop(record);
|
drop(record);
|
||||||
@@ -3201,17 +3274,17 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
fork_candidates_in(root.path(), &identity).len(),
|
fork_candidates_in(root.path(), &identity).len(),
|
||||||
KEEP_UPPERS,
|
KEEP_UPPERS,
|
||||||
"exactly five staged trees remain"
|
"the newest and the newest successful staged trees remain"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A staged tree over the retention size cap is not kept: the
|
/// The build's own tree is kept by its own run whatever its size —
|
||||||
/// `--incremental` request cannot keep a tree that costs more disk
|
/// an `--incremental` user always has at least one staged tree — and
|
||||||
/// than it saves, so the record degrades to metadata-only. The cap
|
/// its measured size is recorded for the sweeps. Sizes are apparent
|
||||||
/// of 0 (unlimited) keeps the same tree. Sizes are apparent sizes,
|
/// sizes, so the 21 GiB probe file is sparse and costs nothing on
|
||||||
/// so the 21 GiB probe file is sparse and costs nothing on disk.
|
/// disk.
|
||||||
#[test]
|
#[test]
|
||||||
fn retain_upper_drops_a_tree_over_the_size_cap() {
|
fn retain_upper_keeps_the_built_tree_over_the_size_cap() {
|
||||||
let (root, tarball) = sessions_fixture();
|
let (root, tarball) = sessions_fixture();
|
||||||
let identity = test_identity();
|
let identity = test_identity();
|
||||||
let host_tree = root.path().join("tree");
|
let host_tree = root.path().join("tree");
|
||||||
@@ -3226,53 +3299,52 @@ mod tests {
|
|||||||
|
|
||||||
oversized.retain_upper_with_cap(true, true, cap);
|
oversized.retain_upper_with_cap(true, true, cap);
|
||||||
assert!(
|
assert!(
|
||||||
!oversized.has_upper(),
|
oversized.has_upper(),
|
||||||
"a tree over the cap must not be kept"
|
"an --incremental run's own tree is always kept"
|
||||||
);
|
);
|
||||||
assert!(!oversized.manifest().kept_upper);
|
assert!(oversized.manifest().kept_upper);
|
||||||
assert!(!oversized.snapshot_file().exists());
|
|
||||||
assert_eq!(oversized.manifest().staged_bytes, None);
|
|
||||||
assert!(oversized.root().join("manifest.json").is_file());
|
|
||||||
assert!(oversized.log_file().is_file());
|
|
||||||
|
|
||||||
// The same tree under an unlimited cap is kept, with its size
|
|
||||||
// recorded for the cap sweeps.
|
|
||||||
let kept = record_with_upper(root.path(), &tarball, &host_tree, &identity, "1.1", None);
|
|
||||||
let file = fs::File::create(kept.upper_dir().join("big.o")).unwrap();
|
|
||||||
file.set_len(over_cap).unwrap();
|
|
||||||
drop(file);
|
|
||||||
kept.retain_upper_with_cap(true, true, 0);
|
|
||||||
assert!(kept.has_upper(), "a cap of 0 keeps any tree");
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
kept.manifest().staged_bytes,
|
oversized.manifest().staged_bytes,
|
||||||
Some(crate::utils::size::tree_sizes(&[kept.upper_dir().as_path()])[0]),
|
Some(crate::utils::size::tree_sizes(&[oversized.upper_dir().as_path()])[0]),
|
||||||
"the recorded size is the tree's measured size"
|
"the recorded size is the tree's measured size"
|
||||||
);
|
);
|
||||||
|
assert!(oversized.root().join("manifest.json").is_file());
|
||||||
|
assert!(oversized.log_file().is_file());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The retention sweep degrades an older record whose staged tree
|
/// The retention sweep applies the size cap to the kept pair: the
|
||||||
/// exceeds the size cap, even though the count cap would keep it:
|
/// newest successful tree over the cap comes down once the failed
|
||||||
/// oversized trees left by older pkh versions or interrupted builds
|
/// newest tree covers the resume — oversized trees left by older pkh
|
||||||
/// come down at the next build's retention.
|
/// versions or interrupted builds come down at the next build's
|
||||||
|
/// retention too.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_cap_sweep_degrades_an_oversized_staged_tree() {
|
fn the_cap_sweep_drops_an_oversized_tree_of_the_pair() {
|
||||||
let (root, tarball) = sessions_fixture();
|
let (root, tarball) = sessions_fixture();
|
||||||
let identity = test_identity();
|
let identity = test_identity();
|
||||||
let host_tree = root.path().join("tree");
|
let host_tree = root.path().join("tree");
|
||||||
let cap = 20 * 1024 * 1024 * 1024;
|
let cap = 20 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
// The newest successful record, staged tree oversized (the
|
||||||
|
// stored measurement stands in for a tree the sweep must not
|
||||||
|
// have to walk).
|
||||||
let old = record_with_upper(root.path(), &tarball, &host_tree, &identity, "1.0", None);
|
let old = record_with_upper(root.path(), &tarball, &host_tree, &identity, "1.0", None);
|
||||||
// The stored measurement stands in for a tree the sweep must not
|
|
||||||
// have to walk: the fixture's own tree is tiny.
|
|
||||||
old.set_staged_bytes(30 * 1024 * 1024 * 1024);
|
old.set_staged_bytes(30 * 1024 * 1024 * 1024);
|
||||||
let old_root = old.root().to_path_buf();
|
let old_root = old.root().to_path_buf();
|
||||||
drop(old); // release the record lock: a held lock is never degraded
|
drop(old); // release the record lock: a held lock is never degraded
|
||||||
|
|
||||||
// The new record's tree is tiny: it is kept, and its retention
|
// The newest record failed: the pair is it (tiny) and the
|
||||||
// sweep takes the old oversized one down.
|
// oversized successful one — the sweep takes the oversized one
|
||||||
let new = record_with_upper(root.path(), &tarball, &host_tree, &identity, "1.1", None);
|
// down while the failed tree covers the resume.
|
||||||
new.retain_upper_with_cap(true, true, cap);
|
let new = record_with_upper(
|
||||||
assert!(new.has_upper(), "the new record's tree is under the cap");
|
root.path(),
|
||||||
|
&tarball,
|
||||||
|
&host_tree,
|
||||||
|
&identity,
|
||||||
|
"1.1",
|
||||||
|
Some(OUTCOME_FAILED),
|
||||||
|
);
|
||||||
|
new.retain_upper_with_cap(true, false, cap);
|
||||||
|
assert!(new.has_upper(), "the newest tree covers the resume");
|
||||||
|
|
||||||
let degraded = identity_records_in(root.path(), &identity)
|
let degraded = identity_records_in(root.path(), &identity)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -3285,6 +3357,54 @@ mod tests {
|
|||||||
assert!(degraded.log_file().is_file());
|
assert!(degraded.log_file().is_file());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The size cap never empties the pair: when both kept trees are
|
||||||
|
/// over the cap, the newest successful one (the auto-fork base) is
|
||||||
|
/// the last one standing and stays.
|
||||||
|
#[test]
|
||||||
|
fn the_cap_sweep_never_drops_the_last_tree_standing() {
|
||||||
|
let (root, tarball) = sessions_fixture();
|
||||||
|
let identity = test_identity();
|
||||||
|
let host_tree = root.path().join("tree");
|
||||||
|
let cap = 20 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
|
let success = record_with_upper(
|
||||||
|
root.path(),
|
||||||
|
&tarball,
|
||||||
|
&host_tree,
|
||||||
|
&identity,
|
||||||
|
"1.0",
|
||||||
|
Some(OUTCOME_SUCCESS),
|
||||||
|
);
|
||||||
|
success.set_staged_bytes(30 * 1024 * 1024 * 1024);
|
||||||
|
let success_root = success.root().to_path_buf();
|
||||||
|
drop(success);
|
||||||
|
|
||||||
|
// The newest record failed, its own tree over the cap as well:
|
||||||
|
// staged manually (the fixture helper retains internally, which
|
||||||
|
// would run the sweep before the tree is oversized).
|
||||||
|
let failed = new_record_in(root.path(), &host_tree, &identity, "1.1").unwrap();
|
||||||
|
fs::create_dir_all(failed.upper_dir().join("src")).unwrap();
|
||||||
|
fs::write(failed.upper_dir().join("src/main.o"), b"objects").unwrap();
|
||||||
|
let file = fs::File::create(failed.upper_dir().join("big.o")).unwrap();
|
||||||
|
file.set_len(30 * 1024 * 1024 * 1024).unwrap();
|
||||||
|
drop(file);
|
||||||
|
failed.set_outcome(OUTCOME_FAILED);
|
||||||
|
failed.retain_upper_with_cap(true, false, cap);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
failed.has_upper(),
|
||||||
|
"an --incremental run's own tree is always kept"
|
||||||
|
);
|
||||||
|
let success = identity_records_in(root.path(), &identity)
|
||||||
|
.into_iter()
|
||||||
|
.find(|r| r.root() == success_root)
|
||||||
|
.expect("the successful record is still there");
|
||||||
|
assert!(
|
||||||
|
success.has_upper(),
|
||||||
|
"the last tree standing is never dropped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The end-of-run retention: metadata and the log are kept for every
|
/// The end-of-run retention: metadata and the log are kept for every
|
||||||
/// build; the staged tree exactly for `--incremental` ones.
|
/// build; the staged tree exactly for `--incremental` ones.
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3338,14 +3458,78 @@ mod tests {
|
|||||||
fn upper_retention_reflects_the_disk_not_the_request() {
|
fn upper_retention_reflects_the_disk_not_the_request() {
|
||||||
use UpperRetention::{Drop, Keep, KeepNothing};
|
use UpperRetention::{Drop, Keep, KeepNothing};
|
||||||
|
|
||||||
// An unlimited cap (0) and a generous one keep a tree under them.
|
assert_eq!(upper_retention(true, true), Keep);
|
||||||
assert_eq!(upper_retention(true, true, 5, 0), Keep);
|
assert_eq!(upper_retention(true, false), KeepNothing);
|
||||||
assert_eq!(upper_retention(true, true, 5, 10), Keep);
|
assert_eq!(upper_retention(false, true), Drop);
|
||||||
// Over the cap the tree is dropped, not kept half-claimed.
|
assert_eq!(upper_retention(false, false), Drop);
|
||||||
assert_eq!(upper_retention(true, true, 11, 10), Drop);
|
}
|
||||||
assert_eq!(upper_retention(true, false, 5, 10), KeepNothing);
|
|
||||||
assert_eq!(upper_retention(false, true, 5, 10), Drop);
|
/// The retention plan: the newest staged tree and the newest
|
||||||
assert_eq!(upper_retention(false, false, 5, 10), Drop);
|
/// successful one are kept, everything else degrades by count.
|
||||||
|
#[test]
|
||||||
|
fn the_retention_plan_keeps_the_pair() {
|
||||||
|
use UpperDecision::{DropCount, Keep};
|
||||||
|
let plan = |records: &[(bool, u64)], max: u64| {
|
||||||
|
upper_retention_plan(
|
||||||
|
&records
|
||||||
|
.iter()
|
||||||
|
.map(|(success, bytes)| UpperPlanRecord {
|
||||||
|
success: *success,
|
||||||
|
bytes: *bytes,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
max,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Newest-first: [failed, success, failed]. The pair is the two
|
||||||
|
// first distinct ones — the newest failed tree and the newest
|
||||||
|
// successful one.
|
||||||
|
assert_eq!(
|
||||||
|
plan(&[(false, 1), (true, 1), (false, 1)], 0),
|
||||||
|
vec![Keep, Keep, DropCount]
|
||||||
|
);
|
||||||
|
// A successful newest record is both members at once: only it
|
||||||
|
// survives.
|
||||||
|
assert_eq!(plan(&[(true, 1), (true, 1)], 0), vec![Keep, DropCount]);
|
||||||
|
// No successful record at all: the newest tree is the one kept
|
||||||
|
// (a failed record is a legitimate explicit-fork base).
|
||||||
|
assert_eq!(plan(&[(false, 1), (false, 1)], 0), vec![Keep, DropCount]);
|
||||||
|
assert_eq!(plan(&[], 0), Vec::<UpperDecision>::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The size cap thins the pair without ever emptying it: the failed
|
||||||
|
/// newest tree yields first, the successful one only while another
|
||||||
|
/// tree remains, and a cap of 0 never applies.
|
||||||
|
#[test]
|
||||||
|
fn the_retention_plan_applies_the_size_cap_to_the_pair() {
|
||||||
|
use UpperDecision::{DropSize, Keep};
|
||||||
|
let plan = |records: &[(bool, u64)], max: u64| {
|
||||||
|
upper_retention_plan(
|
||||||
|
&records
|
||||||
|
.iter()
|
||||||
|
.map(|(success, bytes)| UpperPlanRecord {
|
||||||
|
success: *success,
|
||||||
|
bytes: *bytes,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
max,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The oversized failed newest tree yields to the successful one.
|
||||||
|
assert_eq!(plan(&[(false, 30), (true, 1)], 20), vec![DropSize, Keep]);
|
||||||
|
// The oversized failed tree yields, the next-successful is over
|
||||||
|
// the cap too but is the last tree standing: kept.
|
||||||
|
assert_eq!(plan(&[(false, 30), (true, 30)], 20), vec![DropSize, Keep]);
|
||||||
|
// A failed newest tree under the cap keeps the pair; the
|
||||||
|
// oversized successful tree is dropped while the failed one
|
||||||
|
// still covers the resume.
|
||||||
|
assert_eq!(plan(&[(false, 1), (true, 30)], 20), vec![Keep, DropSize]);
|
||||||
|
// Both members under the cap: untouched.
|
||||||
|
assert_eq!(plan(&[(false, 1), (true, 20)], 20), vec![Keep, Keep]);
|
||||||
|
// The cap of 0 (unlimited) keeps anything.
|
||||||
|
assert_eq!(plan(&[(false, 99), (true, 99)], 0), vec![Keep, Keep]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Driving the keep path on an incremental record whose staged tree
|
/// Driving the keep path on an incremental record whose staged tree
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ pub mod apt;
|
|||||||
pub mod build;
|
pub mod build;
|
||||||
/// Parse or edit a Debian changelog of a source package
|
/// Parse or edit a Debian changelog of a source package
|
||||||
pub mod changelog;
|
pub mod changelog;
|
||||||
/// User settings: the optional `config.json` in the pkh configuration
|
/// User settings: the optional `config.yml` in the pkh configuration
|
||||||
/// directory, its defaults and the environment overrides
|
/// directory, its defaults and the environment overrides
|
||||||
pub mod config;
|
pub mod config;
|
||||||
/// Embedding convention for static reference data (`data/*.yml`), applied
|
/// Embedding convention for static reference data (`data/*.yml`), applied
|
||||||
|
|||||||
+2
-2
@@ -393,7 +393,7 @@ fn main() {
|
|||||||
.require_equals(false)
|
.require_equals(false)
|
||||||
.default_missing_value("")
|
.default_missing_value("")
|
||||||
.help("Incremental build: fork a previous build record of this target and keep its staged tree")
|
.help("Incremental build: fork a previous build record of this target and keep its staged tree")
|
||||||
.long_help("Incremental build: fork a previous build record of this target and keep its staged tree, so make recompiles only what changed. The base's staged tree carries over only while the changelog version and the chroot tarball are unchanged.\nWithout a value, the base is the newest successful record of the target among the last five carrying a staged tree. With an ID as shown by 'pkh deb list', that exact record. A failed record is fine as a base if its staged tree is still there.\nEvery --incremental run keeps its staged tree, success or failure, so the next run can fork it. A plain 'pkh deb' keeps only metadata and the build log."),
|
.long_help("Incremental build: fork a previous build record of this target and keep its staged tree, so make recompiles only what changed. The base's staged tree carries over only while the changelog version and the chroot tarball are unchanged.\nWithout a value, the base is the newest successful record of the target — pkh keeps the newest and the newest successful staged trees per target. With an ID as shown by 'pkh deb list', that exact record. A failed record is fine as a base if its staged tree is still there.\nEvery --incremental run keeps its staged tree, success or failure, so the next run can fork it. A plain 'pkh deb' keeps only metadata and the build log."),
|
||||||
)
|
)
|
||||||
.arg(arg!(--"no-ccache" "Build without the shared ccache").required(false))
|
.arg(arg!(--"no-ccache" "Build without the shared ccache").required(false))
|
||||||
.arg(arg!(--verbose "Show raw tool output instead of the live build view").required(false)
|
.arg(arg!(--verbose "Show raw tool output instead of the live build view").required(false)
|
||||||
@@ -401,7 +401,7 @@ fn main() {
|
|||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("list")
|
Command::new("list")
|
||||||
.about("List the build records recorded from this tree")
|
.about("List the build records recorded from this tree")
|
||||||
.long_about("List the build records recorded from this tree (plain runs too, as metadata records), with their outcome, version, fork lineage, whether their staged tree is kept, how long the dependency installation and the package build took, and the id to pass to 'pkh deb --incremental <id>'.\nRead-only: never builds or removes anything (at most five staged trees per target are kept, 'pkh prune' removes records older than a week)."),
|
.long_about("List the build records recorded from this tree (plain runs too, as metadata records), with their outcome, version, fork lineage, whether their staged tree is kept, how long the dependency installation and the package build took, and the id to pass to 'pkh deb --incremental <id>'.\nRead-only: never builds or removes anything (the newest and the newest successful staged trees are kept per target, 'pkh prune' removes records older than a week)."),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("log")
|
Command::new("log")
|
||||||
|
|||||||
+2
-2
@@ -16,8 +16,8 @@
|
|||||||
//! record retention window plus the corrupt leftovers, `--all`
|
//! record retention window plus the corrupt leftovers, `--all`
|
||||||
//! removes every record — recent builds and staged trees included,
|
//! removes every record — recent builds and staged trees included,
|
||||||
//! which is where the space is. A record a live build holds the lock
|
//! which is where the space is. A record a live build holds the lock
|
||||||
//! of is never prunable, whatever the policy (at most five staged
|
//! of is never prunable, whatever the policy (the newest and the
|
||||||
//! trees per target are kept by `pkh deb` itself).
|
//! newest successful staged trees per target are kept by `pkh deb`).
|
||||||
//! - **Shared apt download caches** under `/var/tmp/pkh/apt-cache/`
|
//! - **Shared apt download caches** under `/var/tmp/pkh/apt-cache/`
|
||||||
//! (see [`crate::deb::ephemeral::apt_cache_dir`]): the default policy
|
//! (see [`crate::deb::ephemeral::apt_cache_dir`]): the default policy
|
||||||
//! removes the ones untouched for longer than the retention window,
|
//! removes the ones untouched for longer than the retention window,
|
||||||
|
|||||||
Reference in New Issue
Block a user