pub struct RetentionPolicy {
pub keep_last: usize,
pub buckets: Vec<RetentionBucket>,
pub max_age: Option<Duration>,
pub max_total_bytes: Option<u64>,
}Expand description
Tiered retention policy. The first keep_last snapshots (by recency) are
kept regardless of bucket coverage; each bucket independently selects
representatives at its own granularity; max_age is an absolute upper
bound that drops snapshots older than the threshold even if a recency or
bucket clause would otherwise keep them; and max_total_bytes is a
per-secret disk budget that drops the oldest survivors of all of the
above until the retained history fits. The absolute age cap satisfies the
cleanup-exemption rule in AGENTS.md: every retained entry has a finite
lifetime.
Every clause is subtractive: the count tiers propose a keep-set, and
max_age / max_total_bytes only ever remove from it. Nothing here can
resurrect a snapshot another clause dropped.
Fields§
§keep_last: usize§buckets: Vec<RetentionBucket>§max_age: Option<Duration>Absolute upper bound on how old a snapshot may be and still be
retained. None disables the cap (older snapshots may stick around
indefinitely if keep_last or a long-interval bucket selects them).
Some(d) drops snapshots older than now - d regardless of which
clause would have kept them.
max_total_bytes: Option<u64>Upper bound, in bytes, on the total on-disk size of the snapshots
retained for ONE secret. None disables the budget (the count tiers
then bound the number of versions but not their size, which is what
let a ~1 MiB secret accumulate ~30 MiB of history). Some(n) drops
the OLDEST otherwise-retained snapshots until the rest fit in n,
never going below [MIN_SNAPSHOTS_KEPT_UNDER_BUDGET].
Applied to the size the entries actually occupy on disk, so it bounds disk directly rather than proxying it through a version count.
Beware the 0 asymmetry when constructing this directly.
Some(0) here is the MOST aggressive budget there is — zero bytes
allowed, so eviction runs until it hits the floor. Setting
[SNAPSHOT_BUDGET_ENV] to the string "0" means the OPPOSITE:
[parse_snapshot_budget] maps it to None, i.e. no budget at all.
The env spelling is the operator-facing “turn this off” switch (an
operator typing 0 means “no limit”, not “limit of nothing”), while
the field is the internal numeric bound and Some(0) is its natural
extreme. Tests rely on the aggressive Some(0) reading. If you add a
constructor that takes a byte count from anywhere operator-facing,
route it through [parse_snapshot_budget] rather than wrapping it in
Some yourself.
This clause is RETROACTIVE. Unlike the count tiers, which a node
upgrading into them satisfies gradually, the first thinning pass after
this field starts being set collapses an existing over-budget history
down to the budget in one go, and those deletions are irreversible.
That is intended (it is how the ~30 MiB histories get reclaimed) but it
is a one-way door, so it is documented for operators in
docs/secrets-at-rest.md and overridable via [SNAPSHOT_BUDGET_ENV].
It is also why Self::without_byte_budget exists: the RECOVERY path
deliberately does not apply it.
Implementations§
Source§impl RetentionPolicy
impl RetentionPolicy
Sourcepub fn with_budget_from(raw: Option<&str>) -> Self
pub fn with_budget_from(raw: Option<&str>) -> Self
Self::default with the byte budget taken from a raw
[SNAPSHOT_BUDGET_ENV] string, per [parse_snapshot_budget].
Split out from Self::from_env so the override is testable without
touching process-global environ: setenv races every concurrent
getenv in the process, and in a lib-test binary those getenvs are
other tests constructing stores. Tests call this; only from_env
reads the environment.
Sourcepub fn from_env() -> Self
pub fn from_env() -> Self
Self::default with the operator’s [SNAPSHOT_BUDGET_ENV] override
applied.
This is the ONLY place in this module that reads the environment, and
it has no test that mutates environ — see Self::with_budget_from
for why. Production stores are built from this (see
SecretsStore::new); anything that thins with a real byte budget
should use it rather than Self::default, or the operator’s
override silently stops applying.
Sourcepub fn without_byte_budget(&self) -> Self
pub fn without_byte_budget(&self) -> Self
This policy with the byte budget removed; the count tiers and
max_age are untouched.
Used by the RECOVERY path (SecretsStore::restore_snapshot and
freenet secrets snapshot-restore). A restore is what an operator
runs when they are trying to get a value back, and it is the worst
possible moment to garbage-collect history: applying the budget there
could evict several older versions — including the one just restored
FROM — as a side effect of the reversibility snapshot the restore
itself adds. The budget is a disk-pressure heuristic, and a manual
restore is not disk pressure.
This is not an unbounded cleanup exemption: max_age still applies at
the restore, so every entry keeps a finite lifetime, and the exemption
is per-CALL, not a property stamped on the history.
§How long the exemption actually holds
Be precise about this, because the two restore entry points differ:
-
freenet secrets snapshot-restore(the operator-facing path): the exemption holds for as long as the operator needs. The CLI requires the node to be STOPPED (seedocs/secrets-at-rest.md), so no delegate write can intervene, and an operator can list, restore, inspect, and restore again across the full retained history. -
SecretsStore::restore_snapshot(the in-process API): the exemption lasts only until the nextstore_secretfor that secret.store_secretthins with the FULL policy on every call — it is gated on whether snapshots are enabled, not on whether this particular write took a snapshot — so a live node whose delegate touches that secret collapses the restored history to the floor, including on one of the identical re-writes that skip snapshotting. Do NOT read this method as buying a working window on a live node. (Today that API has no production callers; it is exercised by tests and available to embedders.)
Gating store_secret’s thin on needs_snapshot — i.e. not
garbage-collecting on a write that adds nothing to the history —
would widen the live-node window. It is deliberately NOT done here:
it is a real semantic change to when reclaim happens, the supported
recovery procedure is the stopped-node CLI where it buys nothing, and
a write that skips its snapshot still leaves a history that the
operator’s configured budget says is too large.
Sourcepub fn select_keep(
&self,
now: SystemTime,
timestamps: &[SystemTime],
) -> BTreeSet<usize>
pub fn select_keep( &self, now: SystemTime, timestamps: &[SystemTime], ) -> BTreeSet<usize>
Given timestamps sorted ascending, return the indices to KEEP.
Indices not in the returned set should be deleted.
Algorithm: walk newest-first. The first snapshot encountered in each
interval-wide age slot wins that slot; subsequent snapshots in the
same slot are eligible for deletion (unless covered by another tier
or keep_last). Per tier, stop after max_count distinct slots.
Finally, max_age filters: any otherwise-kept entry older than
now - max_age is dropped.
Sourcepub fn select_keep_within_budget(
&self,
now: SystemTime,
entries: &[(SystemTime, u64)],
) -> BTreeSet<usize>
pub fn select_keep_within_budget( &self, now: SystemTime, entries: &[(SystemTime, u64)], ) -> BTreeSet<usize>
Self::select_keep plus the Self::max_total_bytes disk budget.
entries is (timestamp, size_bytes) sorted ASCENDING by timestamp
(same contract as select_keep’s timestamps); the returned indices
are into entries. Everything select_keep already dropped stays
dropped — the budget only ever removes MORE, so a snapshot excluded
by max_age can never be resurrected by having spare byte budget.
Eviction order is OLDEST-first, because the newest snapshot is the
value the most recent write replaced and is what an operator undoing
a bad overwrite reaches for. The floor at
[MIN_SNAPSHOTS_KEPT_UNDER_BUDGET] means a secret whose single
version is larger than the whole budget keeps that version and
overshoots the budget, rather than being left with no history: the
budget is a disk heuristic and must never be the reason user data
stops being recoverable.
Trait Implementations§
Source§impl Clone for RetentionPolicy
impl Clone for RetentionPolicy
Source§fn clone(&self) -> RetentionPolicy
fn clone(&self) -> RetentionPolicy
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for RetentionPolicy
impl Debug for RetentionPolicy
Source§impl Default for RetentionPolicy
impl Default for RetentionPolicy
Source§fn default() -> Self
fn default() -> Self
Default: keep the last 5 snapshots, plus one per minute for the last
10 minutes, one per hour for the last 24 hours, one per day for the
last week, one per week for the last 4 weeks, one per month for the
last 12 months, with an absolute 2-year ceiling so stale snapshots
from secrets that stopped being written eventually age out, and a
[DEFAULT_MAX_SNAPSHOT_BYTES_PER_SECRET] disk budget on top. Worst
case ~62 entries per secret in steady state, and never more than
max(budget, MIN_SNAPSHOTS_KEPT_UNDER_BUDGET x largest snapshot) of
disk.
PURE: every field is a compile-time constant, so two default()
values are always identical. The operator override lives in
RetentionPolicy::from_env, deliberately NOT here — a Default
impl that read environ would make every SecretsStore construction
a getenv, which is a data race against any setenv in the same
process (and unit tests are exactly where setenv happens).
Auto Trait Implementations§
impl Freeze for RetentionPolicy
impl RefUnwindSafe for RetentionPolicy
impl Send for RetentionPolicy
impl Sync for RetentionPolicy
impl Unpin for RetentionPolicy
impl UnsafeUnpin for RetentionPolicy
impl UnwindSafe for RetentionPolicy
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more