Skip to main content

RetentionPolicy

Struct RetentionPolicy 

Source
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

Source

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.

Source

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.

Source

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 (see docs/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 next store_secret for that secret. store_secret thins 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.

Source

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.

Source

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

Source§

fn clone(&self) -> RetentionPolicy

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for RetentionPolicy

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for RetentionPolicy

Source§

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§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> FromRef<T> for T
where T: Clone,

Source§

fn from_ref(input: &T) -> T

Converts to this type from a reference to the input type.
Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more