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:
+77
-35
@@ -1,20 +1,26 @@
|
||||
//! User settings: the optional `config.yml` in the pkh configuration
|
||||
//! 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 user creates it, never generated. A file that cannot be read or
|
||||
//! parsed must not take the whole program down: like the context
|
||||
//! configuration, it is backed up to `<path>.bak` (best effort) so a
|
||||
//! later save cannot silently destroy it, and the defaults apply. Every
|
||||
//! field is individually defaulted (`#[serde(default)]`), so any
|
||||
//! hand-written file may carry a subset of the settings.
|
||||
//! struct level is individually defaulted (`#[serde(default)]`), so any
|
||||
//! hand-written file may carry a subset of the settings:
|
||||
//!
|
||||
//! ```yaml
|
||||
//! deb:
|
||||
//! incremental:
|
||||
//! staged_trees: 2
|
||||
//! ```
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// The pkh configuration directory (the one holding `contexts.json`
|
||||
/// and `config.json`).
|
||||
/// and `config.yml`).
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
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)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
/// The retention size cap of a staged build tree: trees larger than
|
||||
/// this are not retained after a build (the record degrades to
|
||||
/// 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,
|
||||
/// `pkh deb` settings.
|
||||
pub deb: DebSettings,
|
||||
}
|
||||
|
||||
/// The staged-tree retention cap in bytes: `PKH_STAGED_MAXSIZE` overrides
|
||||
/// the settings file, which overrides the default (20 GiB — the scale
|
||||
/// where a carried kernel tree stops earning its disk).
|
||||
pub fn staged_maxsize() -> u64 {
|
||||
if let Ok(text) = std::env::var("PKH_STAGED_MAXSIZE")
|
||||
&& let Some(bytes) = crate::utils::size::parse_size(&text)
|
||||
{
|
||||
return bytes;
|
||||
}
|
||||
file_staged_maxsize()
|
||||
/// The `pkh deb` settings.
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct DebSettings {
|
||||
/// `pkh deb --incremental` settings.
|
||||
pub incremental: IncrementalSettings,
|
||||
}
|
||||
|
||||
/// The settings-file layer of [`staged_maxsize`]: the stored value
|
||||
/// parsed, the default when unset, unparseable, or the file is missing
|
||||
/// or corrupt (a corrupt file is backed up, as with the context
|
||||
/// configuration).
|
||||
fn file_staged_maxsize() -> u64 {
|
||||
const DEFAULT: u64 = 20 * 1024 * 1024 * 1024;
|
||||
/// The `pkh deb --incremental` settings.
|
||||
#[derive(Debug, Clone, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct IncrementalSettings {
|
||||
/// How many staged trees `pkh deb` retains per target: the newer
|
||||
/// 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 {
|
||||
return DEFAULT;
|
||||
};
|
||||
@@ -72,15 +81,18 @@ fn file_staged_maxsize() -> u64 {
|
||||
return DEFAULT;
|
||||
}
|
||||
};
|
||||
crate::utils::size::parse_size(&settings.staged_maxsize).unwrap_or_else(|| {
|
||||
log::warn!(
|
||||
"Settings file {} carries an unparseable staged_maxsize \
|
||||
'{}'; using the default",
|
||||
path.display(),
|
||||
settings.staged_maxsize
|
||||
);
|
||||
DEFAULT
|
||||
})
|
||||
match settings.deb.incremental.staged_trees {
|
||||
Some(0) => {
|
||||
log::warn!(
|
||||
"Settings file {} carries staged_trees: 0; the newest \
|
||||
successful staged tree is always retained, so 1 applies",
|
||||
path.display()
|
||||
);
|
||||
1
|
||||
}
|
||||
Some(count) => count,
|
||||
None => DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user