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:
2026-10-03 23:43:10 +02:00
parent a9a6d2001c
commit 9acf650fc9
8 changed files with 284 additions and 414 deletions
+185 -303
View File
@@ -64,15 +64,6 @@ pub(crate) const SCHEMA_VERSION: u32 = 1;
/// removes it.
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`).
pub const OUTCOME_RUNNING: &str = "running";
/// The build recorded in this record succeeded (`last_outcome`).
@@ -268,11 +259,6 @@ pub struct SessionManifest {
/// before any phase produced a measurement).
#[serde(default, skip_serializing_if = "Option::is_none")]
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
@@ -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.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
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,
/// 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,
/// Beyond the retained set.
Drop,
}
/// 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() {
/// `success` flags, one per record): keep `staged_trees` trees — the
/// newer ones first — and always the newest successful one (the tree
/// the next `--incremental` run forks automatically) and the newest one
/// (what a failed run is re-forked from explicitly). When the budget
/// cannot hold the pair — one staged tree configured with 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.
pub(crate) fn upper_retention_plan(success: &[bool], staged_trees: usize) -> Vec<UpperDecision> {
let mut decisions = vec![UpperDecision::Drop; success.len()];
if success.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
let budget = staged_trees.max(1);
let mut keep: Vec<usize> = (0..budget.min(success.len())).collect();
if let Some(success_idx) = success.iter().position(|succeeded| *succeeded)
&& !keep.contains(&success_idx)
{
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;
}
if budget == 1 {
// The newest and the newest successful are two records and
// only one tree is kept: exactly one of them is successful —
// that one — or neither is, and the most recent stays.
keep = vec![success_idx];
} else {
// Make room for the mandatory member: the oldest filled
// slot yields.
keep.pop();
keep.push(success_idx);
}
}
for &idx in &keep {
@@ -387,25 +355,6 @@ pub(crate) fn upper_retention_plan(
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 {
/// The record root directory.
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
/// logged and never abort the build (the journal is a cache).
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
/// recorded on success (failures already carry theirs), and the staged
/// tree follows [`upper_retention`] — kept exactly for `--incremental`
/// runs that actually staged one, whatever its size (an `--incremental`
/// user always has at least one staged tree); a plain run's tree is
/// dropped and a run that ended before staging keeps nothing, both
/// leaving a metadata-only record (manifest + log). The retention
/// sweep afterwards applies the count and size policies to the
/// 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.
/// runs that actually staged one (the user asked for a carried tree);
/// a plain run's tree is dropped and a run that ended before staging
/// keeps nothing, both leaving a metadata-only record (manifest +
/// log). The retention sweep afterwards applies the staged-tree
/// budget to the 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) {
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
/// seam; the cap usually comes from [`staged_maxsize`]).
pub(crate) fn retain_upper_with_cap(&self, keep_upper: bool, succeeded: bool, max_bytes: u64) {
/// [`Self::retain_upper`] with the staged-tree budget explicit (the
/// test seam; the budget usually comes from [`crate::config`]).
pub(crate) fn retain_upper_keeping(
&self,
keep_upper: bool,
succeeded: bool,
staged_trees: usize,
) {
if succeeded {
self.set_outcome(OUTCOME_SUCCESS);
}
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()) {
UpperRetention::Keep => {
self.with_manifest(|manifest| {
manifest.kept_upper = true;
manifest.staged_bytes = upper_bytes;
});
log::info!(
"Build record {id} kept its staged tree ({}, the next \
--incremental run forks it)",
crate::utils::size::format_bytes(upper_bytes.unwrap_or(0))
"Build record {id} kept its staged tree (the next \
--incremental run forks it)"
);
self.enforce_upper_cap(max_bytes);
self.enforce_upper_cap(staged_trees);
}
UpperRetention::KeepNothing => {
log::info!(
@@ -597,7 +538,7 @@ impl Session {
/// Drop the record's persisted upperdir state: the tree-upper, its
/// 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) {
for path in [self.upper_dir(), self.work_dir(), self.snapshot_file()] {
if let Err(e) = crate::deb::ephemeral::privileged_remove(&path)
@@ -608,22 +549,20 @@ impl Session {
}
self.with_manifest(|manifest| {
manifest.kept_upper = false;
manifest.staged_bytes = None;
});
}
/// Enforce the retention policy for this record's identity: keep the
/// newest staged tree and the newest successful one
/// ([`KEEP_UPPERS`]), degrade every other upper-retaining record to
/// metadata-only (upperdir and snapshot deleted, manifest and log
/// kept), and apply the size cap to the kept pair without ever
/// emptying it — covering trees kept oversized by older pkh versions
/// or interrupted builds too. The record whose build just finished
/// (and any record locked by a live build) is never degraded: an
/// `--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
/// Enforce the retention policy for this record's identity: keep
/// `staged_trees` trees — the newer ones first, the newest
/// successful one always among them (see
/// [`upper_retention_plan`]) — and degrade every other
/// upper-retaining record to metadata-only (upperdir and snapshot
/// deleted, manifest and log kept). The record whose build just
/// finished (and any record locked by a live build) is never
/// degraded: an `--incremental` run's own tree is what the user
/// asked to carry, and the sweep of a later build decides on it
/// 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
// root is two levels up, keeping the cap on the same root the
// record lives in (tests use fixture roots).
@@ -636,20 +575,17 @@ impl Session {
.filter(|record| record.has_upper())
.collect();
uppers.sort_by(|a, b| record_ordering(a, b).reverse());
let plan_records: Vec<UpperPlanRecord> = uppers
let success: Vec<bool> = uppers
.iter()
.map(|record| UpperPlanRecord {
success: record.manifest().last_outcome == OUTCOME_SUCCESS,
bytes: upper_size(record),
})
.map(|record| record.manifest().last_outcome == OUTCOME_SUCCESS)
.collect();
for (record, decision) in uppers
.into_iter()
.zip(upper_retention_plan(&plan_records, max_bytes))
.zip(upper_retention_plan(&success, staged_trees))
{
match decision {
UpperDecision::Keep => {}
UpperDecision::DropCount | UpperDecision::DropSize => {
UpperDecision::Drop => {
if record.root == self.root || lock_is_held(&record.lock_path()).unwrap_or(true)
{
log::debug!(
@@ -659,28 +595,11 @@ impl Session {
);
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
);
}
}
log::debug!(
"Record {} degrades to metadata-only (beyond the \
retained staged trees of {identity})",
record.manifest().id
);
record.wipe_upper();
}
}
@@ -1151,7 +1070,6 @@ fn fresh_manifest(
build_resume: false,
timings: None,
profile: None,
staged_bytes: None,
}
}
@@ -1373,7 +1291,6 @@ pub(crate) fn fork_record_in(
build_resume: false,
timings: None,
profile: None,
staged_bytes: None,
})?;
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();
session.set_tarball_sha256(&sha256_file(tarball).unwrap());
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) {
session.set_outcome(outcome);
}
@@ -2761,7 +2678,6 @@ mod tests {
},
],
}),
staged_bytes: None,
};
let base = tempfile::tempdir().unwrap();
save_manifest(base.path(), &manifest).unwrap();
@@ -3273,100 +3189,22 @@ mod tests {
assert!(degraded.log_file().is_file());
assert_eq!(
fork_candidates_in(root.path(), &identity).len(),
KEEP_UPPERS,
2,
"the newest and the newest successful staged trees remain"
);
}
/// The build's own tree is kept by its own run whatever its size —
/// an `--incremental` user always has at least one staged tree — and
/// its measured size is recorded for the sweeps. Sizes are apparent
/// sizes, so the 21 GiB probe file is sparse and costs nothing on
/// disk.
/// A budget of one keeps the newest successful tree: the sweep of
/// a later build takes both the failed newest tree and any other
/// surplus down, while the just-built tree of the running sweep
/// survives its own lock (it comes down at the next retention).
#[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 identity = test_identity();
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(
root.path(),
&tarball,
@@ -3375,33 +3213,81 @@ mod tests {
"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).
// An older failed record with a staged tree.
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);
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!(
failed.has_upper(),
"an --incremental run's own tree is always kept"
newest.has_upper(),
"the just-built tree survives its own run's lock"
);
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 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!(
success.has_upper(),
"the last tree standing is never dropped"
kept.contains(&"1.1".to_string()),
"the newest successful tree is always kept: {kept:?}"
);
}
@@ -3465,71 +3351,67 @@ mod tests {
}
/// 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]
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,
)
};
use UpperDecision::{Drop, Keep};
// 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]
upper_retention_plan(&[false, true, false], 2),
vec![Keep, Keep, Drop]
);
// 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());
// A successful newest record is both members at once; the
// budget still fills with the next-newer tree.
assert_eq!(
upper_retention_plan(&[true, true, false], 2),
vec![Keep, Keep, Drop]
);
// 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
/// newest tree yields first, the successful one only while another
/// tree remains, and a cap of 0 never applies.
/// A budget of one cannot hold the pair: when only one of the two
/// is successful, that one is kept; otherwise the most recent.
#[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,
)
};
fn the_retention_plan_reduces_the_pair_to_one() {
use UpperDecision::{Drop, Keep};
// 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]);
// Newest failed, older successful: the successful tree wins —
// it is what the next run forks automatically.
assert_eq!(upper_retention_plan(&[false, true], 1), vec![Drop, Keep]);
// No successful tree: the most recent stays.
assert_eq!(upper_retention_plan(&[false, false], 1), vec![Keep, Drop]);
// The newest is successful: it is both members at once.
assert_eq!(upper_retention_plan(&[true, false], 1), vec![Keep, Drop]);
}
/// A budget beyond two fills with the newer trees, the newest
/// successful one always among them.
#[test]
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