Retention / GC planner — guide
How to preview what a retention policy would keep vs reclaim in a repository. The planner computes a decision — it deletes nothing (holger's znippy archives are immutable; carrying a plan out is a delegated archive-set drop).
CLI — the dry-run plan
# Keep the 5 newest versions of every package name in `rust-prod`.
holger-server retention --config holger.ron --repo rust-prod --keep-last-n 5
# Every repository at once, with the per-artifact verdict.
holger-server retention --config holger.ron --keep-last-n 3 --verbose
# A store-size quota: keep the newest, evict oldest-first until under 50 GiB.
holger-server retention --config holger.ron --repo cache \
--max-total-bytes 53687091200
# A keep-min floor + latest-protection off (advanced): never below 2 per name.
holger-server retention --config holger.ron --repo npm-dev \
--max-age-secs 2592000 --keep-min-per-name 2 --no-protect-latest
Output (roll-up per repo, plus --verbose lines):
retention plan (DRY-RUN — deletes nothing) @ 1752440000
rust-prod: 812 names, keep 4060 / reclaim 15380 — kept 9.9 GiB bytes, reclaimable 41.2 GiB bytes
keep crates/serde@1.0.219 (287431 bytes) [protected-latest,within-keep-last-n]
RECLAIM crates/serde@1.0.11 (271004 bytes) [superseded-by-newer]
The policy flags
| Flag | Meaning |
--keep-last-n N |
keep the N newest versions per name |
--max-age-secs S |
reclaim records older than S seconds (needs an ingest time) |
--keep-min-per-name N |
floor: never drop below N versions of a name |
--max-total-bytes B |
store quota: evict oldest-first until the kept set fits |
--no-protect-latest |
turn OFF the "never delete a name's newest" safety net |
--verbose |
print the per-artifact keep/reclaim verdict + reasons |
All are optional and compose; omit them all for a keep-everything preview.
Library — plan in-process
use holger_server_lib::retention::RetentionPolicy;
let policy = RetentionPolicy::new().keep_last(5).quota(50 * 1024 * 1024 * 1024);
let now = /* unix seconds */;
let plan = holger.plan_repo_retention("rust-prod", &policy, now)?;
println!("reclaimable: {} bytes across {} artifacts", plan.reclaimed_bytes, plan.delete_count);
for r in plan.to_delete() {
println!("drop {}@{}", r.id.name, r.id.version);
}
Or drive the pure planner directly over records you already hold:
use holger_server_lib::retention::{plan_retention, ArtifactRecord, RetentionPolicy};
let plan = plan_retention(&records, &RetentionPolicy::new().keep_last(3), now);
Executing a plan (actually reclaim)
The planner only decides; the executor carries a plan out. It is dry-run by default — you must opt in to an actual reclaim.
# Preview (default): prints the KEEP/RECLAIM plan, deletes nothing.
holger-server retention --config holger.ron --repo rust-prod --keep-last-n 5
# Reclaim: drop the superseded artifact bytes, historize the GC to the warehouse.
holger-server retention --config holger.ron --repo rust-prod --keep-last-n 5 --execute
In-process:
use holger_server_lib::retention::{GcOptions, RetentionPolicy};
let policy = RetentionPolicy::new().keep_last(5);
let audit = holger.audit_log(); // the same append-only warehouse the server writes
let report = holger.run_repo_retention(
"rust-prod", &policy, now, &GcOptions::execute(), audit.as_ref(), None,
)?;
println!("reclaimed {} artifacts ({} bytes)", report.deleted_count, report.reclaimed_bytes);
Every delete is content-id verified before the unlink, a protected/latest generation is never dropped, and each reclaim is historized as an AuditAction::Delete record. Pass a jera::jobs::JobStore as the last argument to route the run through the durable job ledger (retention-gc).
Notes
- Version ordering is numeric-aware —
1.10.0is newer than1.9.0, sokeep_last_nkeeps the right ones without a timestamp. max_age_secsneeds an ingest time. The livelist()adapter supplies none today, so age no-ops over it (the library path honours times you pass in). See the deferred note in the design doc.- The plan is deterministic — the same inputs always yield the same plan, so a preview and the executor agree.
- The executor is gated and safe — dry-run by default; an actual reclaim is an explicit
--execute/GcOptions::execute(); deletes are content-id verified and protected generations are refused.