deb,config: the staged-tree budget replaces the size cap
The size cap was the wrong shape: an --incremental user asked for a
carried tree, and the invariant that matters is which trees survive —
the newest successful one (the next run's automatic fork base) and the
newest one (what a failed run is re-forked from explicitly). The cap
is gone, with the tree measurement and manifest field it needed; the
retention planner now works on the records' outcomes alone and fills a
budget: staged_trees staged trees per target, the newer ones first,
the pair always among them. When the budget cannot hold the pair (one
staged tree configured, the newest build failed and an older
successful tree present) only one of the two is successful and that
one is kept; with no successful tree at all, the most recent one is.
The budget is a setting of the nested config file:
deb:
incremental:
staged_trees: 2
(default 2; 0 clamps to 1 with a warning — the newest successful tree
always survives). The fork-base selection no longer truncates the
candidate list to the budget before ranking it: with a budget of one
and a failed newest tree, that truncation hid the successful record
and forked the failed one instead.
This commit is contained in:
@@ -183,26 +183,23 @@ 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 two staged trees: the newest one and the newest
|
Each target keeps two staged trees by default: the newest one and the
|
||||||
successful one — the tree the next `--incremental` run forks
|
newest successful one — the tree the next `--incremental` run forks
|
||||||
automatically, and the tree a failed run can be re-forked from
|
automatically, and the tree a failed run can be re-forked from
|
||||||
explicitly. An `--incremental` run always keeps its own tree, so at
|
explicitly — with any newer trees filling the budget. An
|
||||||
least one staged tree exists even when the newest build failed. Every
|
`--incremental` run always keeps its own tree, so at least one staged
|
||||||
other staged tree degrades to a metadata-only record (manifest and log
|
tree exists even when the newest build failed; every other staged tree
|
||||||
stay).
|
degrades to a metadata-only record (manifest and log stay). The
|
||||||
|
budget is a setting:
|
||||||
A staged tree larger than 20 GiB comes down as well, as soon as the
|
|
||||||
pair has another member covering the resume — a tree that size costs
|
|
||||||
more disk than the resume it buys. The cap is a setting:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# ~/.config/pkh/config.yml (missing file or keys = defaults)
|
# ~/.config/pkh/config.yml (missing file or keys = defaults)
|
||||||
staged_maxsize: 20G
|
deb:
|
||||||
|
incremental:
|
||||||
|
staged_trees: 2
|
||||||
```
|
```
|
||||||
|
|
||||||
`0` disables the size rule entirely, and `PKH_STAGED_MAXSIZE`
|
`pkh prune` removes records older than a week. `pkh prune --dry-run` previews everything it would remove,
|
||||||
overrides the file when set. `pkh prune` removes records older than a
|
|
||||||
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
|
||||||
everything a live build does not hold, recent records and staged trees
|
everything a live build does not hold, recent records and staged trees
|
||||||
included; `-f/--force` skips the size pass and the confirmation
|
included; `-f/--force` skips the size pass and the confirmation
|
||||||
|
|||||||
+77
-35
@@ -1,20 +1,26 @@
|
|||||||
//! User settings: the optional `config.yml` 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.
|
//! and their defaults.
|
||||||
//!
|
//!
|
||||||
//! A missing file carries the defaults — the file is written only when
|
//! A missing file carries the defaults — the file is written only when
|
||||||
//! a user creates it, never generated. A file that cannot be read or
|
//! a user creates it, never generated. A file that cannot be read or
|
||||||
//! parsed must not take the whole program down: like the context
|
//! parsed must not take the whole program down: like the context
|
||||||
//! configuration, it is backed up to `<path>.bak` (best effort) so a
|
//! configuration, it is backed up to `<path>.bak` (best effort) so a
|
||||||
//! later save cannot silently destroy it, and the defaults apply. Every
|
//! later save cannot silently destroy it, and the defaults apply. Every
|
||||||
//! field is individually defaulted (`#[serde(default)]`), so any
|
//! struct level is individually defaulted (`#[serde(default)]`), so any
|
||||||
//! hand-written file may carry a subset of the settings.
|
//! hand-written file may carry a subset of the settings:
|
||||||
|
//!
|
||||||
|
//! ```yaml
|
||||||
|
//! deb:
|
||||||
|
//! incremental:
|
||||||
|
//! staged_trees: 2
|
||||||
|
//! ```
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// The pkh configuration directory (the one holding `contexts.json`
|
/// The pkh configuration directory (the one holding `contexts.json`
|
||||||
/// and `config.json`).
|
/// and `config.yml`).
|
||||||
pub fn config_dir() -> Option<PathBuf> {
|
pub fn config_dir() -> Option<PathBuf> {
|
||||||
directories::ProjectDirs::from("com", "pkh", "pkh").map(|dirs| dirs.config_dir().to_path_buf())
|
directories::ProjectDirs::from("com", "pkh", "pkh").map(|dirs| dirs.config_dir().to_path_buf())
|
||||||
}
|
}
|
||||||
@@ -29,32 +35,35 @@ fn config_path() -> Option<PathBuf> {
|
|||||||
#[derive(Debug, Clone, Deserialize, Default)]
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
/// The retention size cap of a staged build tree: trees larger than
|
/// `pkh deb` settings.
|
||||||
/// this are not retained after a build (the record degrades to
|
pub deb: DebSettings,
|
||||||
/// metadata-only), and existing oversized trees degrade at the next
|
|
||||||
/// build's retention. A size string (`20G`, `512M`, bytes); `0`
|
|
||||||
/// disables the cap.
|
|
||||||
pub staged_maxsize: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The staged-tree retention cap in bytes: `PKH_STAGED_MAXSIZE` overrides
|
/// The `pkh deb` settings.
|
||||||
/// the settings file, which overrides the default (20 GiB — the scale
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
/// where a carried kernel tree stops earning its disk).
|
#[serde(default)]
|
||||||
pub fn staged_maxsize() -> u64 {
|
pub struct DebSettings {
|
||||||
if let Ok(text) = std::env::var("PKH_STAGED_MAXSIZE")
|
/// `pkh deb --incremental` settings.
|
||||||
&& let Some(bytes) = crate::utils::size::parse_size(&text)
|
pub incremental: IncrementalSettings,
|
||||||
{
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
file_staged_maxsize()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The settings-file layer of [`staged_maxsize`]: the stored value
|
/// The `pkh deb --incremental` settings.
|
||||||
/// parsed, the default when unset, unparseable, or the file is missing
|
#[derive(Debug, Clone, Deserialize, Default)]
|
||||||
/// or corrupt (a corrupt file is backed up, as with the context
|
#[serde(default)]
|
||||||
/// configuration).
|
pub struct IncrementalSettings {
|
||||||
fn file_staged_maxsize() -> u64 {
|
/// How many staged trees `pkh deb` retains per target: the newer
|
||||||
const DEFAULT: u64 = 20 * 1024 * 1024 * 1024;
|
/// ones first, with the newest successful one and the newest one
|
||||||
|
/// always among them.
|
||||||
|
pub staged_trees: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many staged trees `pkh deb` retains per target: the setting's
|
||||||
|
/// value, or the default (2 — the newest tree and the newest successful
|
||||||
|
/// one). A configured 0 cannot honor the retention invariant (the
|
||||||
|
/// newest successful tree always survives), so it clamps to 1 with a
|
||||||
|
/// warning.
|
||||||
|
pub fn staged_trees() -> usize {
|
||||||
|
const DEFAULT: usize = 2;
|
||||||
let Some(path) = config_path() else {
|
let Some(path) = config_path() else {
|
||||||
return DEFAULT;
|
return DEFAULT;
|
||||||
};
|
};
|
||||||
@@ -72,15 +81,18 @@ fn file_staged_maxsize() -> u64 {
|
|||||||
return DEFAULT;
|
return DEFAULT;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
crate::utils::size::parse_size(&settings.staged_maxsize).unwrap_or_else(|| {
|
match settings.deb.incremental.staged_trees {
|
||||||
log::warn!(
|
Some(0) => {
|
||||||
"Settings file {} carries an unparseable staged_maxsize \
|
log::warn!(
|
||||||
'{}'; using the default",
|
"Settings file {} carries staged_trees: 0; the newest \
|
||||||
path.display(),
|
successful staged tree is always retained, so 1 applies",
|
||||||
settings.staged_maxsize
|
path.display()
|
||||||
);
|
);
|
||||||
DEFAULT
|
1
|
||||||
})
|
}
|
||||||
|
Some(count) => count,
|
||||||
|
None => DEFAULT,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Back up a corrupt settings file next to itself (best effort), so a
|
/// Back up a corrupt settings file next to itself (best effort), so a
|
||||||
@@ -100,3 +112,33 @@ fn backup(path: &std::path::Path) {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// The nested setting parses; every level may be missing and every
|
||||||
|
/// missing level defaults.
|
||||||
|
#[test]
|
||||||
|
fn settings_parse_the_nested_shape() {
|
||||||
|
let settings: Settings =
|
||||||
|
serde_yaml::from_str("deb:\n incremental:\n staged_trees: 3").unwrap();
|
||||||
|
assert_eq!(settings.deb.incremental.staged_trees, Some(3));
|
||||||
|
|
||||||
|
let settings: Settings = serde_yaml::from_str("deb: {}").unwrap();
|
||||||
|
assert_eq!(settings.deb.incremental.staged_trees, None);
|
||||||
|
|
||||||
|
let settings: Settings = serde_yaml::from_str("{}").unwrap();
|
||||||
|
assert_eq!(settings.deb.incremental.staged_trees, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A value of the wrong type is a file error, not a default for the
|
||||||
|
/// field: the whole file is corrupt (the caller backs it up and
|
||||||
|
/// applies the defaults).
|
||||||
|
#[test]
|
||||||
|
fn a_mistyped_value_is_a_corrupt_file() {
|
||||||
|
let result =
|
||||||
|
serde_yaml::from_str::<Settings>("deb:\n incremental:\n staged_trees: two\n");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+7
-1
@@ -291,7 +291,7 @@ fn resumed_upper_dir(session: Option<&session::Session>, build_resume: bool) ->
|
|||||||
/// chroot-local uid, host-side a subuid). The metadata is hot when the
|
/// chroot-local uid, host-side a subuid). The metadata is hot when the
|
||||||
/// resume sync just walked the same tree.
|
/// resume sync just walked the same tree.
|
||||||
fn has_foreign_owner(root: &Path) -> bool {
|
fn has_foreign_owner(root: &Path) -> bool {
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
||||||
let euid = unsafe { libc::geteuid() };
|
let euid = unsafe { libc::geteuid() };
|
||||||
let mut stack = vec![root.to_path_buf()];
|
let mut stack = vec![root.to_path_buf()];
|
||||||
while let Some(dir) = stack.pop() {
|
while let Some(dir) = stack.pop() {
|
||||||
@@ -302,6 +302,12 @@ fn has_foreign_owner(root: &Path) -> bool {
|
|||||||
let Ok(meta) = fs::symlink_metadata(entry.path()) else {
|
let Ok(meta) = fs::symlink_metadata(entry.path()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
// Overlayfs whiteouts (char device 0:0) are the overlay's
|
||||||
|
// own bookkeeping, root-owned in every build — not the mark
|
||||||
|
// of a setpriv-era tree.
|
||||||
|
if meta.file_type().is_char_device() && meta.rdev() == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if meta.uid() != euid {
|
if meta.uid() != euid {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -891,7 +891,6 @@ fn resolve_record_in(
|
|||||||
session::fork_candidates_in(records_root, identity)
|
session::fork_candidates_in(records_root, identity)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|s| !session::lock_is_held(&s.lock_path()).unwrap_or(true))
|
.filter(|s| !session::lock_is_held(&s.lock_path()).unwrap_or(true))
|
||||||
.take(session::KEEP_UPPERS)
|
|
||||||
.collect();
|
.collect();
|
||||||
for base in session::rank_base_candidates(candidates) {
|
for base in session::rank_base_candidates(candidates) {
|
||||||
let why = if base.manifest().last_outcome == session::OUTCOME_SUCCESS {
|
let why = if base.manifest().last_outcome == session::OUTCOME_SUCCESS {
|
||||||
|
|||||||
+185
-303
@@ -64,15 +64,6 @@ 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 staged trees one identity retains: the newest one and the
|
|
||||||
/// newest successful one — a single record when they coincide. Every
|
|
||||||
/// other upper-retaining record degrades to metadata-only (its upperdir
|
|
||||||
/// and host-tree snapshot are deleted; manifest and log stay): the
|
|
||||||
/// 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";
|
||||||
/// The build recorded in this record succeeded (`last_outcome`).
|
/// The build recorded in this record succeeded (`last_outcome`).
|
||||||
@@ -268,11 +259,6 @@ pub struct SessionManifest {
|
|||||||
/// before any phase produced a measurement).
|
/// before any phase produced a measurement).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub profile: Option<BuildProfile>,
|
pub profile: Option<BuildProfile>,
|
||||||
/// Size of the retained staged tree in bytes, measured at retention
|
|
||||||
/// (`None`: never measured — the record predates the field, its tree
|
|
||||||
/// was kept by an interrupted build, or it retains nothing).
|
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
||||||
pub staged_bytes: Option<u64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State shared through the [`Session`] handle: the manifest plus the
|
/// State shared through the [`Session`] handle: the manifest plus the
|
||||||
@@ -325,60 +311,42 @@ pub(crate) fn upper_retention(requested: bool, upper_exists: bool) -> UpperReten
|
|||||||
/// Why the retention plan keeps or degrades a staged tree.
|
/// Why the retention plan keeps or degrades a staged tree.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum UpperDecision {
|
pub(crate) enum UpperDecision {
|
||||||
/// The newest staged tree, or the newest successful one: kept.
|
/// Within the retained set: the newest staged tree, the newest
|
||||||
|
/// successful one, or one of the newer trees filling the budget.
|
||||||
Keep,
|
Keep,
|
||||||
/// Neither the newest staged tree nor the newest successful one.
|
/// Beyond the retained set.
|
||||||
DropCount,
|
Drop,
|
||||||
/// 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
|
/// The retention plan for upper-retaining records (newest-first
|
||||||
/// input): keep the newest staged tree and the newest successful one,
|
/// `success` flags, one per record): keep `staged_trees` trees — the
|
||||||
/// degrade everything else, and apply the size cap to the pair without
|
/// newer ones first — and always the newest successful one (the tree
|
||||||
/// ever emptying it — an `--incremental` user always has at least one
|
/// the next `--incremental` run forks automatically) and the newest one
|
||||||
/// staged tree. When the cap forces a choice inside the pair, the
|
/// (what a failed run is re-forked from explicitly). When the budget
|
||||||
/// failed newest tree yields first: the successful tree is the one the
|
/// cannot hold the pair — one staged tree configured with the newest
|
||||||
/// next run forks automatically.
|
/// build failed and an older successful tree present — only one of the
|
||||||
pub(crate) fn upper_retention_plan(
|
/// two is successful, and that one is kept; with no successful tree at
|
||||||
records: &[UpperPlanRecord],
|
/// all, the most recent one is.
|
||||||
max_bytes: u64,
|
pub(crate) fn upper_retention_plan(success: &[bool], staged_trees: usize) -> Vec<UpperDecision> {
|
||||||
) -> Vec<UpperDecision> {
|
let mut decisions = vec![UpperDecision::Drop; success.len()];
|
||||||
let mut decisions = vec![UpperDecision::DropCount; records.len()];
|
if success.is_empty() {
|
||||||
if records.is_empty() {
|
|
||||||
return decisions;
|
return decisions;
|
||||||
}
|
}
|
||||||
let mut keep: Vec<usize> = vec![0];
|
let budget = staged_trees.max(1);
|
||||||
if let Some(success_idx) = records.iter().position(|record| record.success)
|
let mut keep: Vec<usize> = (0..budget.min(success.len())).collect();
|
||||||
&& success_idx != 0
|
if let Some(success_idx) = success.iter().position(|succeeded| *succeeded)
|
||||||
|
&& !keep.contains(&success_idx)
|
||||||
{
|
{
|
||||||
keep.push(success_idx);
|
if budget == 1 {
|
||||||
}
|
// The newest and the newest successful are two records and
|
||||||
if max_bytes != 0 {
|
// only one tree is kept: exactly one of them is successful —
|
||||||
// Try the cap on the pair, least valuable first: the failed
|
// that one — or neither is, and the most recent stays.
|
||||||
// newest tree, then the successful one — and never the last
|
keep = vec![success_idx];
|
||||||
// tree standing.
|
} else {
|
||||||
let mut drop_order: Vec<usize> = keep.clone();
|
// Make room for the mandatory member: the oldest filled
|
||||||
drop_order.sort_by_key(|&idx| records[idx].success as usize);
|
// slot yields.
|
||||||
for idx in drop_order {
|
keep.pop();
|
||||||
if keep.len() <= 1 {
|
keep.push(success_idx);
|
||||||
break;
|
|
||||||
}
|
|
||||||
if records[idx].bytes > max_bytes {
|
|
||||||
keep.retain(|&kept| kept != idx);
|
|
||||||
decisions[idx] = UpperDecision::DropSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for &idx in &keep {
|
for &idx in &keep {
|
||||||
@@ -387,25 +355,6 @@ pub(crate) fn upper_retention_plan(
|
|||||||
decisions
|
decisions
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The staged-tree retention cap in bytes (`0` = unlimited), from the
|
|
||||||
/// user settings (see [`crate::config`]).
|
|
||||||
pub(crate) fn staged_maxsize() -> u64 {
|
|
||||||
crate::config::staged_maxsize()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The staged tree's size in bytes of a record: the manifest's stored
|
|
||||||
/// measurement when it has one, a fresh one (recorded for the next
|
|
||||||
/// sweeps) otherwise — a tree only changes while its own build runs,
|
|
||||||
/// so a stored value stays accurate between builds.
|
|
||||||
fn upper_size(record: &Session) -> u64 {
|
|
||||||
if let Some(bytes) = record.manifest().staged_bytes {
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
let bytes = crate::utils::size::tree_sizes(&[record.upper_dir().as_path()])[0];
|
|
||||||
record.set_staged_bytes(bytes);
|
|
||||||
bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
/// The record root directory.
|
/// The record root directory.
|
||||||
pub fn root(&self) -> &Path {
|
pub fn root(&self) -> &Path {
|
||||||
@@ -520,14 +469,6 @@ impl Session {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Record the measured size of the retained staged tree and persist
|
|
||||||
/// it (the upper-cap sweeps reuse it instead of re-walking the tree).
|
|
||||||
pub(crate) fn set_staged_bytes(&self, bytes: u64) {
|
|
||||||
self.with_manifest(|manifest| {
|
|
||||||
manifest.staged_bytes = Some(bytes);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run `f` over the manifest and persist the result; failures are
|
/// Run `f` over the manifest and persist the result; failures are
|
||||||
/// logged and never abort the build (the journal is a cache).
|
/// logged and never abort the build (the journal is a cache).
|
||||||
fn with_manifest(&self, f: impl FnOnce(&mut SessionManifest)) {
|
fn with_manifest(&self, f: impl FnOnce(&mut SessionManifest)) {
|
||||||
@@ -548,39 +489,39 @@ 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, whatever its size (an `--incremental`
|
/// runs that actually staged one (the user asked for a carried tree);
|
||||||
/// user always has at least one staged tree); a plain run's tree is
|
/// a plain run's tree is dropped and a run that ended before staging
|
||||||
/// dropped and a run that ended before staging keeps nothing, both
|
/// keeps nothing, both leaving a metadata-only record (manifest +
|
||||||
/// leaving a metadata-only record (manifest + log). The retention
|
/// log). The retention sweep afterwards applies the staged-tree
|
||||||
/// sweep afterwards applies the count and size policies to the
|
/// budget to the identity's older trees. The chroot was already
|
||||||
/// identity's older trees. The chroot was already removed by the
|
/// removed by the caller's teardown. The interrupt path is handled
|
||||||
/// caller's teardown. The interrupt path is handled by the SIGINT
|
/// by the SIGINT cleanup hook, which follows the same policy.
|
||||||
/// 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_keeping(keep_upper, succeeded, crate::config::staged_trees());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`Self::retain_upper`] with the retention cap explicit (the test
|
/// [`Self::retain_upper`] with the staged-tree budget explicit (the
|
||||||
/// seam; the cap usually comes from [`staged_maxsize`]).
|
/// test seam; the budget usually comes from [`crate::config`]).
|
||||||
pub(crate) fn retain_upper_with_cap(&self, keep_upper: bool, succeeded: bool, max_bytes: u64) {
|
pub(crate) fn retain_upper_keeping(
|
||||||
|
&self,
|
||||||
|
keep_upper: bool,
|
||||||
|
succeeded: bool,
|
||||||
|
staged_trees: usize,
|
||||||
|
) {
|
||||||
if succeeded {
|
if succeeded {
|
||||||
self.set_outcome(OUTCOME_SUCCESS);
|
self.set_outcome(OUTCOME_SUCCESS);
|
||||||
}
|
}
|
||||||
let id = self.manifest().id;
|
let id = self.manifest().id;
|
||||||
let upper_bytes = (keep_upper && self.has_upper())
|
|
||||||
.then(|| crate::utils::size::tree_sizes(&[self.upper_dir().as_path()])[0]);
|
|
||||||
match upper_retention(keep_upper, self.has_upper()) {
|
match upper_retention(keep_upper, self.has_upper()) {
|
||||||
UpperRetention::Keep => {
|
UpperRetention::Keep => {
|
||||||
self.with_manifest(|manifest| {
|
self.with_manifest(|manifest| {
|
||||||
manifest.kept_upper = true;
|
manifest.kept_upper = true;
|
||||||
manifest.staged_bytes = upper_bytes;
|
|
||||||
});
|
});
|
||||||
log::info!(
|
log::info!(
|
||||||
"Build record {id} kept its staged tree ({}, the next \
|
"Build record {id} kept its staged tree (the next \
|
||||||
--incremental run forks it)",
|
--incremental run forks it)"
|
||||||
crate::utils::size::format_bytes(upper_bytes.unwrap_or(0))
|
|
||||||
);
|
);
|
||||||
self.enforce_upper_cap(max_bytes);
|
self.enforce_upper_cap(staged_trees);
|
||||||
}
|
}
|
||||||
UpperRetention::KeepNothing => {
|
UpperRetention::KeepNothing => {
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -597,7 +538,7 @@ impl Session {
|
|||||||
|
|
||||||
/// Drop the record's persisted upperdir state: the tree-upper, its
|
/// Drop the record's persisted upperdir state: the tree-upper, its
|
||||||
/// host-tree snapshot and the scratch workdir, and the manifest's
|
/// host-tree snapshot and the scratch workdir, and the manifest's
|
||||||
/// claims about them (`kept_upper`, the measured size).
|
/// claim about the tree (`kept_upper`).
|
||||||
fn wipe_upper(&self) {
|
fn wipe_upper(&self) {
|
||||||
for path in [self.upper_dir(), self.work_dir(), self.snapshot_file()] {
|
for path in [self.upper_dir(), self.work_dir(), self.snapshot_file()] {
|
||||||
if let Err(e) = crate::deb::ephemeral::privileged_remove(&path)
|
if let Err(e) = crate::deb::ephemeral::privileged_remove(&path)
|
||||||
@@ -608,22 +549,20 @@ impl Session {
|
|||||||
}
|
}
|
||||||
self.with_manifest(|manifest| {
|
self.with_manifest(|manifest| {
|
||||||
manifest.kept_upper = false;
|
manifest.kept_upper = false;
|
||||||
manifest.staged_bytes = None;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enforce the retention policy for this record's identity: keep the
|
/// Enforce the retention policy for this record's identity: keep
|
||||||
/// newest staged tree and the newest successful one
|
/// `staged_trees` trees — the newer ones first, the newest
|
||||||
/// ([`KEEP_UPPERS`]), degrade every other upper-retaining record to
|
/// successful one always among them (see
|
||||||
/// metadata-only (upperdir and snapshot deleted, manifest and log
|
/// [`upper_retention_plan`]) — and degrade every other
|
||||||
/// kept), and apply the size cap to the kept pair without ever
|
/// upper-retaining record to metadata-only (upperdir and snapshot
|
||||||
/// emptying it — covering trees kept oversized by older pkh versions
|
/// deleted, manifest and log kept). The record whose build just
|
||||||
/// or interrupted builds too. The record whose build just finished
|
/// finished (and any record locked by a live build) is never
|
||||||
/// (and any record locked by a live build) is never degraded: an
|
/// degraded: an `--incremental` run's own tree is what the user
|
||||||
/// `--incremental` run's own tree is the at-least-one tree the user
|
/// asked to carry, and the sweep of a later build decides on it
|
||||||
/// asked for, and the sweep of a later build applies the cap to it
|
|
||||||
/// once its lock is free.
|
/// once its lock is free.
|
||||||
fn enforce_upper_cap(&self, max_bytes: u64) {
|
fn enforce_upper_cap(&self, staged_trees: usize) {
|
||||||
// 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
|
||||||
// record lives in (tests use fixture roots).
|
// record lives in (tests use fixture roots).
|
||||||
@@ -636,20 +575,17 @@ 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());
|
||||||
let plan_records: Vec<UpperPlanRecord> = uppers
|
let success: Vec<bool> = uppers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|record| UpperPlanRecord {
|
.map(|record| record.manifest().last_outcome == OUTCOME_SUCCESS)
|
||||||
success: record.manifest().last_outcome == OUTCOME_SUCCESS,
|
|
||||||
bytes: upper_size(record),
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
for (record, decision) in uppers
|
for (record, decision) in uppers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.zip(upper_retention_plan(&plan_records, max_bytes))
|
.zip(upper_retention_plan(&success, staged_trees))
|
||||||
{
|
{
|
||||||
match decision {
|
match decision {
|
||||||
UpperDecision::Keep => {}
|
UpperDecision::Keep => {}
|
||||||
UpperDecision::DropCount | UpperDecision::DropSize => {
|
UpperDecision::Drop => {
|
||||||
if record.root == self.root || lock_is_held(&record.lock_path()).unwrap_or(true)
|
if record.root == self.root || lock_is_held(&record.lock_path()).unwrap_or(true)
|
||||||
{
|
{
|
||||||
log::debug!(
|
log::debug!(
|
||||||
@@ -659,28 +595,11 @@ impl Session {
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match decision {
|
log::debug!(
|
||||||
UpperDecision::DropSize => {
|
"Record {} degrades to metadata-only (beyond the \
|
||||||
log::info!(
|
retained staged trees of {identity})",
|
||||||
"Record {} degrades to metadata-only (its \
|
record.manifest().id
|
||||||
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();
|
record.wipe_upper();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1151,7 +1070,6 @@ fn fresh_manifest(
|
|||||||
build_resume: false,
|
build_resume: false,
|
||||||
timings: None,
|
timings: None,
|
||||||
profile: None,
|
profile: None,
|
||||||
staged_bytes: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1373,7 +1291,6 @@ pub(crate) fn fork_record_in(
|
|||||||
build_resume: false,
|
build_resume: false,
|
||||||
timings: None,
|
timings: None,
|
||||||
profile: None,
|
profile: None,
|
||||||
staged_bytes: None,
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if carry_upper {
|
if carry_upper {
|
||||||
@@ -2430,7 +2347,7 @@ pub(crate) mod record_fixtures {
|
|||||||
fs::write(session.snapshot_file(), "f\t1\t10\ttree/src/a.c\n").unwrap();
|
fs::write(session.snapshot_file(), "f\t1\t10\ttree/src/a.c\n").unwrap();
|
||||||
session.set_tarball_sha256(&sha256_file(tarball).unwrap());
|
session.set_tarball_sha256(&sha256_file(tarball).unwrap());
|
||||||
session.record_phase("patches", Some("stamp".to_string()));
|
session.record_phase("patches", Some("stamp".to_string()));
|
||||||
session.retain_upper(true, outcome == Some(OUTCOME_SUCCESS));
|
session.retain_upper_keeping(true, outcome == Some(OUTCOME_SUCCESS), 2);
|
||||||
if let Some(outcome) = outcome.filter(|o| *o != OUTCOME_SUCCESS) {
|
if let Some(outcome) = outcome.filter(|o| *o != OUTCOME_SUCCESS) {
|
||||||
session.set_outcome(outcome);
|
session.set_outcome(outcome);
|
||||||
}
|
}
|
||||||
@@ -2761,7 +2678,6 @@ mod tests {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
staged_bytes: None,
|
|
||||||
};
|
};
|
||||||
let base = tempfile::tempdir().unwrap();
|
let base = tempfile::tempdir().unwrap();
|
||||||
save_manifest(base.path(), &manifest).unwrap();
|
save_manifest(base.path(), &manifest).unwrap();
|
||||||
@@ -3273,100 +3189,22 @@ mod tests {
|
|||||||
assert!(degraded.log_file().is_file());
|
assert!(degraded.log_file().is_file());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fork_candidates_in(root.path(), &identity).len(),
|
fork_candidates_in(root.path(), &identity).len(),
|
||||||
KEEP_UPPERS,
|
2,
|
||||||
"the newest and the newest successful staged trees remain"
|
"the newest and the newest successful staged trees remain"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The build's own tree is kept by its own run whatever its size —
|
/// A budget of one keeps the newest successful tree: the sweep of
|
||||||
/// an `--incremental` user always has at least one staged tree — and
|
/// a later build takes both the failed newest tree and any other
|
||||||
/// its measured size is recorded for the sweeps. Sizes are apparent
|
/// surplus down, while the just-built tree of the running sweep
|
||||||
/// sizes, so the 21 GiB probe file is sparse and costs nothing on
|
/// survives its own lock (it comes down at the next retention).
|
||||||
/// disk.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retain_upper_keeps_the_built_tree_over_the_size_cap() {
|
fn a_budget_of_one_keeps_the_newest_successful_tree() {
|
||||||
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 over_cap = 21 * 1024 * 1024 * 1024;
|
|
||||||
let cap = 20 * 1024 * 1024 * 1024;
|
|
||||||
|
|
||||||
let oversized =
|
|
||||||
record_with_upper(root.path(), &tarball, &host_tree, &identity, "1.0", None);
|
|
||||||
let file = fs::File::create(oversized.upper_dir().join("big.o")).unwrap();
|
|
||||||
file.set_len(over_cap).unwrap();
|
|
||||||
drop(file);
|
|
||||||
|
|
||||||
oversized.retain_upper_with_cap(true, true, cap);
|
|
||||||
assert!(
|
|
||||||
oversized.has_upper(),
|
|
||||||
"an --incremental run's own tree is always kept"
|
|
||||||
);
|
|
||||||
assert!(oversized.manifest().kept_upper);
|
|
||||||
assert_eq!(
|
|
||||||
oversized.manifest().staged_bytes,
|
|
||||||
Some(crate::utils::size::tree_sizes(&[oversized.upper_dir().as_path()])[0]),
|
|
||||||
"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 applies the size cap to the kept pair: the
|
|
||||||
/// newest successful tree over the cap comes down once the failed
|
|
||||||
/// newest tree covers the resume — oversized trees left by older pkh
|
|
||||||
/// versions or interrupted builds come down at the next build's
|
|
||||||
/// retention too.
|
|
||||||
#[test]
|
|
||||||
fn the_cap_sweep_drops_an_oversized_tree_of_the_pair() {
|
|
||||||
let (root, tarball) = sessions_fixture();
|
|
||||||
let identity = test_identity();
|
|
||||||
let host_tree = root.path().join("tree");
|
|
||||||
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);
|
|
||||||
old.set_staged_bytes(30 * 1024 * 1024 * 1024);
|
|
||||||
let old_root = old.root().to_path_buf();
|
|
||||||
drop(old); // release the record lock: a held lock is never degraded
|
|
||||||
|
|
||||||
// The newest record failed: the pair is it (tiny) and the
|
|
||||||
// oversized successful one — the sweep takes the oversized one
|
|
||||||
// down while the failed tree covers the resume.
|
|
||||||
let new = record_with_upper(
|
|
||||||
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)
|
|
||||||
.into_iter()
|
|
||||||
.find(|r| r.root() == old_root)
|
|
||||||
.expect("the old record is still there");
|
|
||||||
assert!(!degraded.has_upper(), "the oversized tree is gone");
|
|
||||||
assert!(!degraded.manifest().kept_upper);
|
|
||||||
assert_eq!(degraded.manifest().staged_bytes, None);
|
|
||||||
assert!(degraded.root().join("manifest.json").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;
|
|
||||||
|
|
||||||
|
// The newest successful record: the auto-fork base.
|
||||||
let success = record_with_upper(
|
let success = record_with_upper(
|
||||||
root.path(),
|
root.path(),
|
||||||
&tarball,
|
&tarball,
|
||||||
@@ -3375,33 +3213,81 @@ mod tests {
|
|||||||
"1.0",
|
"1.0",
|
||||||
Some(OUTCOME_SUCCESS),
|
Some(OUTCOME_SUCCESS),
|
||||||
);
|
);
|
||||||
success.set_staged_bytes(30 * 1024 * 1024 * 1024);
|
|
||||||
let success_root = success.root().to_path_buf();
|
let success_root = success.root().to_path_buf();
|
||||||
drop(success);
|
drop(success);
|
||||||
|
|
||||||
// The newest record failed, its own tree over the cap as well:
|
// An older failed record with a staged tree.
|
||||||
// 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();
|
let failed = new_record_in(root.path(), &host_tree, &identity, "1.1").unwrap();
|
||||||
fs::create_dir_all(failed.upper_dir().join("src")).unwrap();
|
fs::create_dir_all(failed.upper_dir().join("src")).unwrap();
|
||||||
fs::write(failed.upper_dir().join("src/main.o"), b"objects").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.set_outcome(OUTCOME_FAILED);
|
||||||
failed.retain_upper_with_cap(true, false, cap);
|
let failed_root = failed.root().to_path_buf();
|
||||||
|
drop(failed);
|
||||||
|
|
||||||
|
// The newest build failed as well, its tree staged: under a
|
||||||
|
// budget of one the successful tree is the one kept.
|
||||||
|
let newest = new_record_in(root.path(), &host_tree, &identity, "1.2").unwrap();
|
||||||
|
fs::create_dir_all(newest.upper_dir().join("src")).unwrap();
|
||||||
|
fs::write(newest.upper_dir().join("src/main.o"), b"objects").unwrap();
|
||||||
|
newest.set_outcome(OUTCOME_FAILED);
|
||||||
|
newest.retain_upper_keeping(true, false, 1);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
failed.has_upper(),
|
newest.has_upper(),
|
||||||
"an --incremental run's own tree is always kept"
|
"the just-built tree survives its own run's lock"
|
||||||
);
|
);
|
||||||
let success = identity_records_in(root.path(), &identity)
|
let success = identity_records_in(root.path(), &identity)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|r| r.root() == success_root)
|
.find(|r| r.root() == success_root)
|
||||||
.expect("the successful record is still there");
|
.expect("the successful record is still there");
|
||||||
|
assert!(success.has_upper(), "the newest successful tree is kept");
|
||||||
|
let failed = identity_records_in(root.path(), &identity)
|
||||||
|
.into_iter()
|
||||||
|
.find(|r| r.root() == failed_root)
|
||||||
|
.expect("the failed record is still there");
|
||||||
|
assert!(!failed.has_upper(), "the surplus failed tree is gone");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A budget beyond two fills with the newer trees, the newest
|
||||||
|
/// successful one always among them.
|
||||||
|
#[test]
|
||||||
|
fn a_budget_of_three_fills_with_the_newer_trees() {
|
||||||
|
let (root, tarball) = sessions_fixture();
|
||||||
|
let identity = test_identity();
|
||||||
|
let host_tree = root.path().join("tree");
|
||||||
|
|
||||||
|
// Newest-first after the newest build: [newest, 1.2, 1.1, 1.0]
|
||||||
|
// with 1.1 the newest successful one.
|
||||||
|
for (version, outcome) in [("1.0", None), ("1.1", Some(OUTCOME_SUCCESS)), ("1.2", None)] {
|
||||||
|
let record = record_with_upper(
|
||||||
|
root.path(),
|
||||||
|
&tarball,
|
||||||
|
&host_tree,
|
||||||
|
&identity,
|
||||||
|
version,
|
||||||
|
outcome,
|
||||||
|
);
|
||||||
|
drop(record);
|
||||||
|
}
|
||||||
|
let surplus = new_record_in(root.path(), &host_tree, &identity, "1.3").unwrap();
|
||||||
|
fs::create_dir_all(surplus.upper_dir().join("src")).unwrap();
|
||||||
|
fs::write(surplus.upper_dir().join("src/main.o"), b"objects").unwrap();
|
||||||
|
surplus.set_outcome(OUTCOME_FAILED);
|
||||||
|
surplus.retain_upper_keeping(true, false, 3);
|
||||||
|
|
||||||
|
let kept = identity_records_in(root.path(), &identity)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|record| record.has_upper())
|
||||||
|
.map(|record| record.manifest().tree_version)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
kept.len(),
|
||||||
|
3,
|
||||||
|
"the budget bounds the retained trees: {kept:?}"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
success.has_upper(),
|
kept.contains(&"1.1".to_string()),
|
||||||
"the last tree standing is never dropped"
|
"the newest successful tree is always kept: {kept:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3465,71 +3351,67 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The retention plan: the newest staged tree and the newest
|
/// The retention plan: the newest staged tree and the newest
|
||||||
/// successful one are kept, everything else degrades by count.
|
/// successful one are kept, everything else degrades; the budget
|
||||||
|
/// fills with the newer trees.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_retention_plan_keeps_the_pair() {
|
fn the_retention_plan_keeps_the_pair() {
|
||||||
use UpperDecision::{DropCount, Keep};
|
use UpperDecision::{Drop, 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
|
// Newest-first: [failed, success, failed]. The pair is the two
|
||||||
// first distinct ones — the newest failed tree and the newest
|
// first distinct ones — the newest failed tree and the newest
|
||||||
// successful one.
|
// successful one.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plan(&[(false, 1), (true, 1), (false, 1)], 0),
|
upper_retention_plan(&[false, true, false], 2),
|
||||||
vec![Keep, Keep, DropCount]
|
vec![Keep, Keep, Drop]
|
||||||
);
|
);
|
||||||
// A successful newest record is both members at once: only it
|
// A successful newest record is both members at once; the
|
||||||
// survives.
|
// budget still fills with the next-newer tree.
|
||||||
assert_eq!(plan(&[(true, 1), (true, 1)], 0), vec![Keep, DropCount]);
|
assert_eq!(
|
||||||
// No successful record at all: the newest tree is the one kept
|
upper_retention_plan(&[true, true, false], 2),
|
||||||
// (a failed record is a legitimate explicit-fork base).
|
vec![Keep, Keep, Drop]
|
||||||
assert_eq!(plan(&[(false, 1), (false, 1)], 0), vec![Keep, DropCount]);
|
);
|
||||||
assert_eq!(plan(&[], 0), Vec::<UpperDecision>::new());
|
// No successful record at all: the newest tree is the one the
|
||||||
|
// pair keeps (a failed record is a legitimate explicit-fork
|
||||||
|
// base), and the budget fills with the next ones.
|
||||||
|
assert_eq!(
|
||||||
|
upper_retention_plan(&[false, false, false], 2),
|
||||||
|
vec![Keep, Keep, Drop]
|
||||||
|
);
|
||||||
|
assert_eq!(upper_retention_plan(&[], 2), Vec::<UpperDecision>::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The size cap thins the pair without ever emptying it: the failed
|
/// A budget of one cannot hold the pair: when only one of the two
|
||||||
/// newest tree yields first, the successful one only while another
|
/// is successful, that one is kept; otherwise the most recent.
|
||||||
/// tree remains, and a cap of 0 never applies.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_retention_plan_applies_the_size_cap_to_the_pair() {
|
fn the_retention_plan_reduces_the_pair_to_one() {
|
||||||
use UpperDecision::{DropSize, Keep};
|
use UpperDecision::{Drop, 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.
|
// Newest failed, older successful: the successful tree wins —
|
||||||
assert_eq!(plan(&[(false, 30), (true, 1)], 20), vec![DropSize, Keep]);
|
// it is what the next run forks automatically.
|
||||||
// The oversized failed tree yields, the next-successful is over
|
assert_eq!(upper_retention_plan(&[false, true], 1), vec![Drop, Keep]);
|
||||||
// the cap too but is the last tree standing: kept.
|
// No successful tree: the most recent stays.
|
||||||
assert_eq!(plan(&[(false, 30), (true, 30)], 20), vec![DropSize, Keep]);
|
assert_eq!(upper_retention_plan(&[false, false], 1), vec![Keep, Drop]);
|
||||||
// A failed newest tree under the cap keeps the pair; the
|
// The newest is successful: it is both members at once.
|
||||||
// oversized successful tree is dropped while the failed one
|
assert_eq!(upper_retention_plan(&[true, false], 1), vec![Keep, Drop]);
|
||||||
// still covers the resume.
|
}
|
||||||
assert_eq!(plan(&[(false, 1), (true, 30)], 20), vec![Keep, DropSize]);
|
|
||||||
// Both members under the cap: untouched.
|
/// A budget beyond two fills with the newer trees, the newest
|
||||||
assert_eq!(plan(&[(false, 1), (true, 20)], 20), vec![Keep, Keep]);
|
/// successful one always among them.
|
||||||
// The cap of 0 (unlimited) keeps anything.
|
#[test]
|
||||||
assert_eq!(plan(&[(false, 99), (true, 99)], 0), vec![Keep, Keep]);
|
fn the_retention_plan_fills_the_budget() {
|
||||||
|
use UpperDecision::{Drop, Keep};
|
||||||
|
|
||||||
|
// Newest-first: the success sits within the fill.
|
||||||
|
assert_eq!(
|
||||||
|
upper_retention_plan(&[false, false, true, false], 3),
|
||||||
|
vec![Keep, Keep, Keep, Drop]
|
||||||
|
);
|
||||||
|
// The success sits beyond the fill: it displaces the oldest
|
||||||
|
// filled slot.
|
||||||
|
assert_eq!(
|
||||||
|
upper_retention_plan(&[false, false, false, true], 3),
|
||||||
|
vec![Keep, Keep, Drop, Keep]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Driving the keep path on an incremental record whose staged tree
|
/// Driving the keep path on an incremental record whose staged tree
|
||||||
|
|||||||
+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 — 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."),
|
.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 (two by default, 'deb.incremental.staged_trees' in ~/.config/pkh/config.yml). 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 (the newest and the newest successful staged trees are kept per target, '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 by default, 'pkh prune' removes records older than a week)."),
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("log")
|
Command::new("log")
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,8 @@
|
|||||||
//! 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 (the newest and the
|
//! of is never prunable, whatever the policy (the newest and the
|
||||||
//! newest successful staged trees per target are kept by `pkh deb`).
|
//! newest successful staged trees per target are kept by `pkh deb`,
|
||||||
|
//! two by default).
|
||||||
//! - **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,
|
||||||
|
|||||||
@@ -111,35 +111,6 @@ pub(crate) fn format_bytes(bytes: u64) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a human size into bytes: an integer with an optional unit
|
|
||||||
/// suffix — `B`, `K`, `M`, `G`, `T` (optionally `iB`, case-insensitive),
|
|
||||||
/// the binary multiples ccache's configuration uses (`20G` = 20 GiB). A
|
|
||||||
/// bare number is bytes. `None` on anything else (empty, negative,
|
|
||||||
/// overflow, unknown unit).
|
|
||||||
pub(crate) fn parse_size(text: &str) -> Option<u64> {
|
|
||||||
let text = text.trim();
|
|
||||||
let split = text
|
|
||||||
.rfind(|c: char| c.is_ascii_digit())
|
|
||||||
.map(|idx| idx + 1)
|
|
||||||
.unwrap_or(0);
|
|
||||||
let (digits, unit) = text.split_at(split);
|
|
||||||
let power = match unit.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"" | "b" => 0,
|
|
||||||
"k" | "kib" => 1,
|
|
||||||
"m" | "mib" => 2,
|
|
||||||
"g" | "gib" => 3,
|
|
||||||
"t" | "tib" => 4,
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
digits
|
|
||||||
.parse::<u64>()
|
|
||||||
.ok()
|
|
||||||
.and_then(|value| value.checked_mul(1u64.checked_shl(10 * power)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -188,32 +159,4 @@ mod tests {
|
|||||||
// this size; the mount-crossing walk keeps it out of the totals.
|
// this size; the mount-crossing walk keeps it out of the totals.
|
||||||
assert_eq!(format_bytes(140_737_477_959_680), "128.0 TiB");
|
assert_eq!(format_bytes(140_737_477_959_680), "128.0 TiB");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unit spellings and their byte values: ccache-style binary
|
|
||||||
/// multiples, optional B/iB decorations, case-insensitive.
|
|
||||||
#[test]
|
|
||||||
fn parse_size_reads_the_unit_suffixes() {
|
|
||||||
assert_eq!(parse_size("0"), Some(0));
|
|
||||||
assert_eq!(parse_size("512"), Some(512));
|
|
||||||
assert_eq!(parse_size("512B"), Some(512));
|
|
||||||
assert_eq!(parse_size("512 b"), Some(512));
|
|
||||||
assert_eq!(parse_size("10K"), Some(10 * 1024));
|
|
||||||
assert_eq!(parse_size("512m"), Some(512 * 1024 * 1024));
|
|
||||||
assert_eq!(parse_size("20G"), Some(20 * 1024 * 1024 * 1024));
|
|
||||||
assert_eq!(parse_size("2TiB"), Some(2 * 1024 * 1024 * 1024 * 1024));
|
|
||||||
assert_eq!(parse_size(" 20G "), Some(20 * 1024 * 1024 * 1024));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Garbage is an error, not a guess: empty, negative, junk units and
|
|
||||||
/// overflow all parse to None.
|
|
||||||
#[test]
|
|
||||||
fn parse_size_rejects_garbage() {
|
|
||||||
assert_eq!(parse_size(""), None);
|
|
||||||
assert_eq!(parse_size(" "), None);
|
|
||||||
assert_eq!(parse_size("G"), None);
|
|
||||||
assert_eq!(parse_size("-1"), None);
|
|
||||||
assert_eq!(parse_size("20GB"), None);
|
|
||||||
assert_eq!(parse_size("20 kg"), None);
|
|
||||||
assert_eq!(parse_size("999999999999999999999999T"), None);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user