Skip to main content

haematite_cli/serve/storage/
release.rs

1//! STOR-003 HR1: what a branch's versions are relied on by, which a release
2//! never passes (graph-and-query-surface.md, "What a version is relied on
3//! by"): the head; a branch forked from it, while that branch exists; an open
4//! merge's three moments; a discard asked for and not finished; and the
5//! embedding job's position in each declared field (V4).
6//!
7//! The cost is one tree read for each live branch (its fork origin), one
8//! paged read of the open merge rows, and the pending discards; never in
9//! proportion to the branch's history. The branches read are counted so the
10//! answer can show it.
11
12use std::collections::BTreeMap;
13use std::num::{NonZeroU64, NonZeroUsize};
14
15use haematite::SyncCounts;
16use haematite::branch::history::Row;
17use haematite::branch::{CommitDurability, CommitRequest, KeptHead, Timestamp};
18use haematite::db::ReadOnlyDatabase;
19use haematite::tree::Hash;
20use serde::Serialize;
21
22use super::branches::provenance;
23use super::dead;
24use super::embedded;
25use super::history::{self, Decision, Released};
26use super::{
27    BRANCH_PREFIX, BranchRefRecord, Captured, Failure, SERVICE, Storage, branch_name, identifier,
28    open_branch, version,
29};
30use crate::serve::protocol::Version;
31
32/// What relies on a version.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
34#[serde(rename_all = "snake_case")]
35pub enum Source {
36    Head,
37    Fork,
38    Merge,
39    Discard,
40    /// The embedding job's finished `through` or its open walk's target,
41    /// named `collection.field`: its next page reads both.
42    Embed,
43    /// Another branch's companion position naming this branch, as a fork's
44    /// inherits its parent's (F2): named `branch:collection.field`.
45    EmbedOf,
46    /// The branch's own setting to keep its newest versions (STOR-003 HR3).
47    KeepVersions,
48    /// The branch's own setting to keep versions superseded recently.
49    KeepDays,
50}
51
52/// One thing relying on a version of the branch: what, by name, and which.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
54pub struct HeldBy {
55    pub source: Source,
56    /// The branch for a head or a fork, the merge id, or the discarded branch.
57    pub name: String,
58    pub version: Version,
59}
60
61/// Everything relying on a branch's versions, oldest first.
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
63pub struct Floor {
64    /// Ordered by (generation, sequence), oldest first. The head is among
65    /// them when asked of [`Storage::relied_on`]; asked of
66    /// [`Storage::relied_on_by_others`] it is not, and the list may be empty.
67    pub held_by: Vec<HeldBy>,
68    /// Live branches whose fork origin was read.
69    pub branches_read: usize,
70}
71
72impl Floor {
73    /// The oldest version something relies on: a release takes only what is
74    /// below it. `None` when nothing relies on any version: every version may
75    /// go.
76    pub fn oldest(&self) -> Option<&HeldBy> {
77        self.held_by.first()
78    }
79
80    /// The oldest version relied on in `generation` (DR-6): a version holds
81    /// the rows of its own generation only, so a hold on an older generation
82    /// never holds a newer one back.
83    pub fn oldest_in(&self, generation: u64) -> Option<Version> {
84        self.held_by
85            .iter()
86            .map(|held| held.version)
87            .filter(|version| version.generation == generation)
88            .min_by_key(|version| order(*version))
89    }
90}
91
92/// The pending discards as the store lists them from their intents
93/// (STOR-003 H1b2-B): what the release driver reads its floor with.
94pub struct IntentListing;
95
96impl PendingDiscards for IntentListing {
97    fn pending(&self, storage: &Storage) -> Result<Vec<(String, Version)>, Failure> {
98        storage.pending_discards()
99    }
100}
101
102/// The discards asked for and not finished, as (branch, version) each.
103pub trait PendingDiscards {
104    /// # Errors
105    /// Whatever stops the pending discards from being read.
106    fn pending(&self, storage: &Storage) -> Result<Vec<(String, Version)>, Failure>;
107}
108
109/// The pending discards until the intent listing in `discards.rs` can be
110/// called from here (STOR-003 H1b2): refused by name, so no release ever runs
111/// without them. Nothing outside the tests asks for a floor yet (HR3 will),
112/// so only they read it.
113#[cfg(test)]
114pub struct DiscardsUnread;
115
116#[cfg(test)]
117impl PendingDiscards for DiscardsUnread {
118    fn pending(&self, _storage: &Storage) -> Result<Vec<(String, Version)>, Failure> {
119        Err(Failure::new(
120            "release_discards_unread",
121            "the pending discards cannot be read yet, so nothing is released",
122        ))
123    }
124}
125
126impl Storage {
127    /// Everything relying on `branch`'s versions now, its head among them
128    /// (STOR-003 HR1).
129    ///
130    /// # Errors
131    /// `branch_missing`; whatever reading a fork origin, a merge row or the
132    /// pending discards answers.
133    pub fn relied_on(
134        &self,
135        branch: &str,
136        discards: &dyn PendingDiscards,
137    ) -> Result<Floor, Failure> {
138        identifier(branch)?;
139        let head = self
140            .refs
141            .get(&branch_name(branch))
142            .ok_or_else(|| Failure::new("branch_missing", branch))?;
143        let head = HeldBy {
144            source: Source::Head,
145            name: branch.to_owned(),
146            version: version(head),
147        };
148        let mut floor = self.relied_on_by_others(branch, false, discards)?;
149        floor.held_by.push(head);
150        floor
151            .held_by
152            .sort_by_key(|held| (held.version.generation, held.version.sequence));
153        Ok(floor)
154    }
155
156    /// Everything other than `branch`'s own head relying on its versions:
157    /// the branches forked from it, the open merges pinning it, and its
158    /// pending discard unless `skip_own_discard` (STOR-003 `HR1b`). Its ref may
159    /// be hidden or gone, as a discard being finished has it.
160    ///
161    /// An empty floor means nothing relies on any version of `branch`, so
162    /// every version of it may go; it never means that nothing may.
163    ///
164    /// # Errors
165    /// Whatever reading a fork origin, a merge row or the pending discards
166    /// answers.
167    pub(super) fn relied_on_by_others(
168        &self,
169        branch: &str,
170        skip_own_discard: bool,
171        discards: &dyn PendingDiscards,
172    ) -> Result<Floor, Failure> {
173        identifier(branch)?;
174        let mut held_by = Vec::new();
175        let mut branches_read = 0_usize;
176        for record in self.refs.list_prefix(BRANCH_PREFIX) {
177            let Some(fork) = record.name.strip_prefix(BRANCH_PREFIX) else {
178                continue;
179            };
180            if fork == branch {
181                continue;
182            }
183            let at = Captured {
184                branch: fork.to_owned(),
185                version: version(record),
186                shards: record.shards.clone(),
187            };
188            branches_read = branches_read.saturating_add(1);
189            if let Some(origin) = provenance(&self.reader, &at)?
190                && origin.branch == branch
191            {
192                held_by.push(HeldBy {
193                    source: Source::Fork,
194                    name: fork.to_owned(),
195                    version: origin.version,
196                });
197            }
198        }
199        for plan in self.open_merge_plans()? {
200            for pin in [&plan.origin, &plan.source, &plan.target] {
201                if pin.branch == branch {
202                    held_by.push(HeldBy {
203                        source: Source::Merge,
204                        name: plan.id.clone(),
205                        version: pin.version,
206                    });
207                }
208            }
209        }
210        // One pending discard at most names a branch (reconcile refuses two),
211        // so skipping the branch's entries skips exactly its own.
212        for (discarded, at) in discards.pending(self)? {
213            if discarded == branch && !skip_own_discard {
214                held_by.push(HeldBy {
215                    source: Source::Discard,
216                    name: discarded,
217                    version: at,
218                });
219            }
220        }
221        held_by.extend(self.embed_positions(branch)?);
222        held_by.sort_by_key(|held| (held.version.generation, held.version.sequence));
223        Ok(Floor {
224            held_by,
225            branches_read,
226        })
227    }
228}
229
230impl Storage {
231    /// The versions of `branch` the embedding job's positions hold, each on
232    /// the branch it is a version of (F2b): a position's `through` on the
233    /// branch it names, from its own companion (V4) or another's, as a
234    /// fork's inherited one names its parent (F2); an open walk's target on
235    /// the companion's owner. Every companion is read, one
236    /// paged read of its position keys each, so a companion whose branch is
237    /// pending discard holds until that discard finishes; the reads are in
238    /// proportion to the companions and their declared fields.
239    fn embed_positions(&self, branch: &str) -> Result<Vec<HeldBy>, Failure> {
240        let mut held = Vec::new();
241        for record in self.refs.list_prefix(embedded::COMPANION_PREFIX) {
242            let Some(owner) = record.name.strip_prefix(embedded::COMPANION_PREFIX) else {
243                continue;
244            };
245            for ((collection, field), position) in self.companion_positions(record)? {
246                let field = format!("{collection}.{field}");
247                // `through` is a version of the branch the position names.
248                if position.branch == branch
249                    && let Some(through) = position.through
250                {
251                    let (source, name) = if owner == branch {
252                        (Source::Embed, field.clone())
253                    } else {
254                        (Source::EmbedOf, format!("{owner}:{field}"))
255                    };
256                    held.push(HeldBy {
257                        source,
258                        name,
259                        version: through,
260                    });
261                }
262                // An open walk's target is always the owner's own version
263                // (F-2, V2c), whatever branch the position names.
264                if owner == branch
265                    && let Some(open) = position.toward
266                {
267                    held.push(HeldBy {
268                        source: Source::Embed,
269                        name: field,
270                        version: open.version,
271                    });
272                }
273            }
274        }
275        Ok(held)
276    }
277
278    /// Every position in a companion, by (collection, field), in key order:
279    /// one paged read of its position keys. The one reader of that layout,
280    /// for the floor and for a fork's inheriting (F1b).
281    ///
282    /// # Errors
283    /// `embed_position_key_corrupt` for a key that is not UTF-8 or names no
284    /// field; `embed_position_corrupt`; whatever a read answers.
285    pub(super) fn companion_positions(
286        &self,
287        companion: &BranchRefRecord,
288    ) -> Result<BTreeMap<(String, String), embedded::Position>, Failure> {
289        let limit = NonZeroUsize::new(POSITIONS_A_PAGE)
290            .ok_or_else(|| Failure::new("embed_positions_page", "a page must be nonzero"))?;
291        let mut positions = BTreeMap::new();
292        for shard in &companion.shards {
293            let mut from = b"e\0".to_vec();
294            loop {
295                let page = self
296                    .reader
297                    .range_page_at(shard.shard_id, shard.head, &from, b"e\x01", limit)
298                    .map_err(Failure::observer)?;
299                for (key, bytes) in page.entries {
300                    let field = position_field(&companion.name, &key)?;
301                    positions.insert(field, embedded::read_position(&bytes)?);
302                }
303                let Some(next) = page.next_from else {
304                    break;
305                };
306                from = next;
307            }
308        }
309        Ok(positions)
310    }
311}
312
313/// The (collection, field) a position key names, refused by name, never
314/// guessed, when it is not UTF-8 or has no field part. The key's bytes are
315/// never printed.
316fn position_field(companion: &str, key: &[u8]) -> Result<(String, String), Failure> {
317    let corrupt = || {
318        Failure::new(
319            "embed_position_key_corrupt",
320            format!("{companion} holds a position key that names no collection and field"),
321        )
322    };
323    let named = key.strip_prefix(b"e\0".as_slice()).ok_or_else(corrupt)?;
324    let named = std::str::from_utf8(named).map_err(|_error| corrupt())?;
325    let (collection, field) = named.split_once('\0').ok_or_else(corrupt)?;
326    Ok((collection.to_owned(), field.to_owned()))
327}
328
329/// Position keys read a page at a time.
330const POSITIONS_A_PAGE: usize = 64;
331
332/// What a branch keeps, resolved from the store's settings and the branch's
333/// exception (ruled 07:24). Each setting left `None` keeps all (the design's
334/// default), so the default keeps every version.
335#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
336pub struct Keep {
337    /// The newest this many versions, the head included.
338    pub versions: Option<NonZeroU64>,
339    /// Versions superseded fewer than this many days ago.
340    pub days: Option<u64>,
341}
342
343/// How one release judges and bounds what it takes.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub struct Rules {
346    /// `None` judges no keep settings at all, as a finishing discard asks
347    /// (ruled 08:05); never the same as `Some(Keep::default())`, which keeps
348    /// every version.
349    pub keep: Option<Keep>,
350    /// `retention.release_max_rows`: the most one release may name.
351    pub max_rows: NonZeroUsize,
352    /// Now, in nanoseconds since the epoch, as row timestamps are.
353    pub now: Timestamp,
354}
355
356/// The oldest and newest version one release took.
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
358pub struct Span {
359    pub first: Version,
360    pub last: Version,
361}
362
363/// What `history.release` answers (ruled 07:24, 8).
364#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
365pub struct Answer {
366    pub branch: String,
367    pub released: Option<Span>,
368    pub marker: Option<Released>,
369    /// What stopped the release short of its limit; empty when nothing did.
370    pub held_by: Vec<HeldBy>,
371    /// Rows still qualify beyond the limit.
372    pub more: bool,
373    pub branches_read: usize,
374    /// Whether a reclaim pass was asked for after the release.
375    pub pass_asked: bool,
376    /// Why asking for that pass was refused, when it was.
377    pub pass_refused: Option<String>,
378    /// When the row the keep days hold falls due: when it was superseded,
379    /// plus the days. `None` when no row is held by days with a known
380    /// superseding time. Read from the rows this release read, nothing more.
381    pub due_at: Option<Timestamp>,
382}
383
384const NANOS_A_DAY: u64 = 86_400 * 1_000_000_000;
385
386/// Where a branch's first release starts: below every version it has.
387const LOWEST: Version = Version {
388    generation: 0,
389    sequence: 0,
390};
391
392const fn order(version: Version) -> (u64, u64) {
393    (version.generation, version.sequence)
394}
395
396const fn row_version(row: &Row) -> Version {
397    Version {
398        generation: row.key.generation,
399        sequence: row.key.sequence,
400    }
401}
402
403/// The newest a release may reach: `version` and below never, and when it
404/// was written. A release passes the head; a finishing discard passes the
405/// version after its intent's, so the intent's own row may go.
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub struct Below {
408    pub version: Version,
409    pub written: Timestamp,
410}
411
412/// What the oldest `cap` of `rows` may go, judged against the floor and the
413/// keep settings: the versions taken, what stopped them, and whether the row
414/// after the cap would have gone too. `rows` runs from the oldest kept row
415/// towards `below`, at most `cap + 1` of them. An empty floor bounds nothing:
416/// only `below` does.
417///
418/// DR-8: until something is taken, rows something relies on are passed, not
419/// stopped at: a fork holding an older generation of a live name holds that
420/// generation only, so the newer rows behind it in the same read are judged.
421/// What was passed counts towards `cap`, and is what held the release when
422/// nothing is taken. Taken rows stay one span: nothing is passed after the
423/// first is taken.
424fn judge(
425    branch: &str,
426    rows: &[Row],
427    cap: usize,
428    floor: &Floor,
429    below: Below,
430    rules: &Rules,
431) -> (Vec<Version>, Vec<HeldBy>, bool) {
432    let mut taken = Vec::new();
433    let mut passed = Vec::new();
434    for (index, row) in rows.iter().enumerate() {
435        let at = row_version(row);
436        if order(at) >= order(below.version) {
437            break;
438        }
439        let held = held_at(branch, rows, index, floor, below, rules);
440        if index == cap {
441            // Rows passed fill the cap without a release: the next would read
442            // and pass the same ones, so no more is asked for.
443            let more = held.is_empty() && !taken.is_empty();
444            let held_by = if taken.is_empty() { passed } else { Vec::new() };
445            return (taken, held_by, more);
446        }
447        if !held.is_empty() {
448            if taken.is_empty() && floor_holds(floor, at) {
449                if passed.is_empty() {
450                    passed = held;
451                }
452                continue;
453            }
454            return (taken, held, false);
455        }
456        taken.push(at);
457    }
458    let held_by = if taken.is_empty() { passed } else { Vec::new() };
459    (taken, held_by, false)
460}
461
462/// Where a release reads its rows: from `from` towards `below`, at most
463/// `limit` + 1 a read, and whether the name is live (DR-13).
464#[derive(Debug, Clone, Copy)]
465struct ReadFrom {
466    from: Version,
467    below: Below,
468    limit: NonZeroUsize,
469    live: bool,
470}
471
472/// What `judge` answers: the rows taken, what held the release, and more.
473type Judged = (Vec<Version>, Vec<HeldBy>, bool);
474
475/// DR-13: the start of the generation after the last of `rows`' first `cap`,
476/// when the read filled its cap with rows `floor` relies on and that
477/// generation is older than `below`'s; `None` otherwise.
478fn past_held(rows: &[Row], cap: usize, floor: &Floor, below: Version) -> Option<Version> {
479    if rows.len() <= cap {
480        return None;
481    }
482    let passed = rows.get(..cap)?;
483    if !passed
484        .iter()
485        .all(|row| floor_holds(floor, row_version(row)))
486    {
487        return None;
488    }
489    let generation = passed.last()?.key.generation;
490    (generation < below.generation).then(|| Version {
491        generation: generation.saturating_add(1),
492        sequence: 0,
493    })
494}
495
496/// Whether `floor` relies on `at` or a version before it in its generation.
497fn floor_holds(floor: &Floor, at: Version) -> bool {
498    let generation = at.generation;
499    floor
500        .oldest_in(generation)
501        .is_some_and(|oldest| order(at) >= order(oldest))
502}
503
504/// The rows a decision left behind names, read no further than its count:
505/// from its first version, at most `count` + 1 rows, those up to its last.
506/// A number found other than its count refuses by name, and the decision
507/// is left as it is: never guessed (HR4-fix2).
508fn decided_rows(
509    reader: &ReadOnlyDatabase,
510    service: &BranchRefRecord,
511    branch: &str,
512    decision: &Decision,
513    below: Below,
514) -> Result<Vec<Version>, Failure> {
515    let read = NonZeroUsize::new(decision.count.saturating_add(1)).unwrap_or(NonZeroUsize::MAX);
516    let rows = history::oldest_rows(
517        reader,
518        service,
519        branch,
520        (decision.first, below.version),
521        read,
522    )?;
523    let taken: Vec<Version> = rows
524        .iter()
525        .map(row_version)
526        .filter(|at| order(*at) <= order(decision.last))
527        .collect();
528    if taken.len() != decision.count {
529        return Err(Failure::new(
530            "release_decision_mismatch",
531            format!(
532                "the release decided for {branch} names {} rows from {}.{} to {}.{}, and {} \
533                 are there; the decision is left",
534                decision.count,
535                decision.first.generation,
536                decision.first.sequence,
537                decision.last.generation,
538                decision.last.sequence,
539                taken.len()
540            ),
541        ));
542    }
543    Ok(taken)
544}
545
546/// What keeps `rows[index]`: empty when it may go.
547fn held_at(
548    branch: &str,
549    rows: &[Row],
550    index: usize,
551    floor: &Floor,
552    below: Below,
553    rules: &Rules,
554) -> Vec<HeldBy> {
555    let Some(row) = rows.get(index) else {
556        return Vec::new();
557    };
558    let at = row_version(row);
559    if let Some(oldest) = floor.oldest_in(at.generation)
560        && order(at) >= order(oldest)
561    {
562        return floor
563            .held_by
564            .iter()
565            .filter(|held| held.version == oldest)
566            .cloned()
567            .collect();
568    }
569    let Some(keep) = rules.keep else {
570        return Vec::new();
571    };
572    let kept = |source| {
573        vec![HeldBy {
574            source,
575            name: branch.to_owned(),
576            version: at,
577        }]
578    };
579    let Some(versions) = keep.versions else {
580        return kept(Source::KeepVersions);
581    };
582    let Some(days) = keep.days else {
583        return kept(Source::KeepDays);
584    };
585    let head = below.version;
586    let newer = newer_than(rows, index, head);
587    if newer < versions.get() {
588        return kept(Source::KeepVersions);
589    }
590    // Superseded when the next version was written: the next row's
591    // timestamp, or the head's for the last row. Unknown is kept.
592    let superseded = match rows.get(index + 1) {
593        Some(next) => Some(next.timestamp),
594        None if order(at) < order(head) && rows.len() <= index + 1 => None,
595        None => Some(below.written),
596    };
597    let old_enough = superseded.is_some_and(|superseded| {
598        rules.now.saturating_sub(superseded) >= days.saturating_mul(NANOS_A_DAY)
599    });
600    if !old_enough {
601        return kept(Source::KeepDays);
602    }
603    Vec::new()
604}
605
606/// When the row the keep days hold falls due: the next row's timestamp, which
607/// is when it was superseded, plus the days. `None` when the days hold none,
608/// or hold the last row read, whose superseding time is unknown.
609fn due_at(rows: &[Row], held_by: &[HeldBy], rules: &Rules) -> Option<Timestamp> {
610    let days = rules.keep?.days?;
611    let held = held_by
612        .iter()
613        .find(|held| held.source == Source::KeepDays)?;
614    let index = rows
615        .iter()
616        .position(|row| row_version(row) == held.version)?;
617    let superseded = rows.get(index + 1)?.timestamp;
618    Some(superseded.saturating_add(days.saturating_mul(NANOS_A_DAY)))
619}
620
621/// How many versions are newer than `rows[index]`, the head among them, as
622/// far as the rows read show: never more than the truth, so a count short
623/// of a keep setting keeps.
624///
625/// A generation's versions run from sequence 0 up by one: a record is
626/// created at seq 0 (haematite `branch/lifecycle.rs`, `create_branch_record`;
627/// `branch/fork_changes.rs` for a fork) and each advance writes
628/// `expected_seq + 1` only over a record at `expected_seq`
629/// (`branch/refstore.rs`, `advance_with_source`). So in the head's
630/// generation exactly `head.sequence + 1` versions are newer than a row of
631/// an older generation, and `head.sequence - at.sequence` than a row of its
632/// own. Rows read after an older row are added only where their generation
633/// is older than the head's: the head generation's are already in
634/// `head.sequence + 1`. Versions of an older generation with no row read
635/// are not counted.
636fn newer_than(rows: &[Row], index: usize, head: Version) -> u64 {
637    let Some(row) = rows.get(index) else {
638        return 0;
639    };
640    let at = row_version(row);
641    if at.generation == head.generation {
642        return head.sequence.saturating_sub(at.sequence);
643    }
644    let older_after = rows
645        .get(index + 1..)
646        .unwrap_or_default()
647        .iter()
648        .filter(|later| later.key.generation < head.generation)
649        .count();
650    // Too few counted keeps; a count that cannot be made is none.
651    head.sequence
652        .saturating_add(1)
653        .saturating_add(u64::try_from(older_after).unwrap_or(0))
654}
655
656/// DR-11: an operator's release of a generation discarded before DR-1.
657///
658/// Such a generation has rows and no marker, so no release finds it; naming
659/// it writes its marker, dated by the request, and the ordinary rules then
660/// take its rows.
661#[derive(Debug, Clone, PartialEq, Eq)]
662pub struct UndatedGeneration {
663    pub branch: String,
664    /// The generation: its record's creation time, as its rows are keyed.
665    pub generation: u64,
666    /// Taken as the discard's time, so `keep.days` counts from it.
667    pub at: Timestamp,
668}
669
670/// The rows of one generation a dating request reads at a time.
671const UNDATED_PAGE: usize = 256;
672
673/// A named point in a release's durable order where a test stops it, as a
674/// crash would (STOR-003 HR3).
675#[cfg(test)]
676#[derive(Debug, Clone, Copy, PartialEq, Eq)]
677pub enum Crash {
678    /// The decision row is committed; nothing else is done.
679    AfterDecision,
680    /// Half the history files are removed.
681    DuringFiles,
682    /// Every history file is removed; the rows are not.
683    AfterFiles,
684    /// The rows, the mark and the decision are committed; no pass is asked.
685    AfterCommit,
686    /// The rows of the kept heads past their days are committed; the record
687    /// still keeps them (DR-7).
688    BeforeTrim,
689}
690
691#[cfg(test)]
692thread_local! {
693    static CRASH: std::cell::Cell<Option<Crash>> = const { std::cell::Cell::new(None) };
694}
695
696/// Stop this thread's next release at `point`.
697#[cfg(test)]
698pub fn crash_at(point: Crash) {
699    CRASH.with(|crash| crash.set(Some(point)));
700}
701
702#[cfg(test)]
703fn crashed(point: Crash) -> Result<(), Failure> {
704    if CRASH.with(|crash| crash.get() == Some(point)) {
705        CRASH.with(|crash| crash.set(None));
706        return Err(Failure::new("release_crashed", format!("{point:?}")));
707    }
708    Ok(())
709}
710
711impl Storage {
712    /// `history.release {branch, limit}` (STOR-003 HR3): takes the oldest
713    /// versions of `branch` that nothing relies on and the keep settings let
714    /// go, at most `limit`, in this durable order: one decision row; then the
715    /// older history files, in one ref operation; then the rows, the mark and
716    /// the decision's removal in one commit. A pass is then asked for. A
717    /// decision a crash left is finished here first, taking only what may
718    /// still go and saying what held the rest.
719    ///
720    /// Every version taken reads whole until that last commit: its row is
721    /// still in the service tree, and every reclaim pass marks from the
722    /// service's rows (STOR-003 R2), whether or not its older file is gone.
723    ///
724    /// No second release runs beside a decision's owner: this takes the
725    /// storage mutably, the service holds it behind one lock, and another
726    /// process is kept out by the writer lock. A call that meets a decision
727    /// left behind finishes that decision as it was written, asking no keep
728    /// setting, hold or limit again (HR4-fix2), and decides nothing new.
729    ///
730    /// # Errors
731    /// `release_limit_exceeded` for a limit above `rules.max_rows`; whatever
732    /// the floor, a read or a commit answers.
733    pub fn release(
734        &mut self,
735        branch: &str,
736        limit: Option<NonZeroUsize>,
737        rules: &Rules,
738        discards: &dyn PendingDiscards,
739    ) -> Result<Answer, Failure> {
740        let answer = self.release_once(branch, limit, rules, discards);
741        // Ended, whatever it took and whether or not it failed: the driver's
742        // count for the branch starts again, so neither a branch its settings
743        // keep whole nor one whose release fails is asked again until
744        // another batch of advances (STOR-003 HR4, HR4-fix).
745        self.kept_rows.released(branch);
746        answer
747    }
748
749    /// Every live branch, for the driver to ask once at start (STOR-003
750    /// HR4-fix): the records in memory, no read.
751    #[must_use]
752    pub fn branch_names(&self) -> Vec<String> {
753        self.refs
754            .list_prefix(BRANCH_PREFIX)
755            .filter_map(|record| record.name.strip_prefix(BRANCH_PREFIX))
756            .map(str::to_owned)
757            .collect()
758    }
759
760    /// The branches whose release a crash left decided and unfinished:
761    /// one range read of the decision rows, never the history (STOR-003
762    /// HR4). A decision whose branch has no live record is not named.
763    ///
764    /// # Errors
765    /// Storage failures.
766    pub fn decided_branches(&self) -> Result<Vec<String>, Failure> {
767        let service = self
768            .refs
769            .get(SERVICE)
770            .ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
771        let decided = history::decided(&self.reader, service)?;
772        Ok(self
773            .refs
774            .list_prefix(BRANCH_PREFIX)
775            .filter_map(|record| record.name.strip_prefix(BRANCH_PREFIX))
776            .filter(|name| decided.contains(&Hash::of(name.as_bytes())))
777            .map(str::to_owned)
778            .collect())
779    }
780
781    /// `branch`'s advances since its last release (STOR-003 HR4).
782    #[must_use]
783    pub fn advanced_since_release(&self, branch: &str) -> usize {
784        self.kept_rows.advanced(branch)
785    }
786
787    /// The durable operations the store has made, for the release driver to
788    /// count its own by (STOR-003 HR4).
789    #[must_use]
790    pub fn sync_counts(&self) -> SyncCounts {
791        self.writer.sync_counts()
792    }
793
794    fn release_once(
795        &mut self,
796        branch: &str,
797        limit: Option<NonZeroUsize>,
798        rules: &Rules,
799        discards: &dyn PendingDiscards,
800    ) -> Result<Answer, Failure> {
801        let limit = match limit {
802            Some(limit) if limit > rules.max_rows => {
803                return Err(Failure::new(
804                    "release_limit_exceeded",
805                    format!(
806                        "limit {limit} is above retention.release_max_rows {}",
807                        rules.max_rows
808                    ),
809                ));
810            }
811            Some(limit) => limit,
812            None => rules.max_rows,
813        };
814        let service = self
815            .refs
816            .get(SERVICE)
817            .cloned()
818            .ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
819        let live = self.refs.get(&branch_name(branch)).is_some();
820        let pending = history::read_decision(&self.reader, &service, branch)?;
821        // A decision left behind is the commit point: it is rolled forward
822        // as written, and no keep setting, hold or limit is asked again
823        // (Apollo 14:24, HR4-fix2). Its files may already be gone.
824        let floor = match (&pending, live) {
825            (Some(_decision), _) => None,
826            (None, true) => Some(self.relied_on(branch, discards)?),
827            (None, false) => Some(self.relied_on_by_others(branch, false, discards)?),
828        };
829        let (below, mark) =
830            self.below(&service, branch, pending.as_ref(), floor.as_ref(), limit)?;
831        let (taken, held_by, more, branches_read, due) = if let Some(decision) = &pending {
832            let taken = decided_rows(&self.reader, &service, branch, decision, below)?;
833            (taken, Vec::new(), false, 0, None)
834        } else {
835            let floor = floor.unwrap_or_default();
836            let from = if live {
837                LOWEST
838            } else {
839                past_mark(mark, below.version)
840            };
841            let read = ReadFrom {
842                from,
843                below,
844                limit,
845                live,
846            };
847            let (rows, (taken, held_by, more)) =
848                self.judge_from(&service, branch, read, &floor, rules)?;
849            let mut due = due_at(&rows, &held_by, rules);
850            if live {
851                let trim_due = self.trim_past_days(branch, rules)?;
852                due = due.into_iter().chain(trim_due).min();
853            }
854            (taken, held_by, more, floor.branches_read, due)
855        };
856        let mut answer = Answer {
857            branch: branch.to_owned(),
858            released: None,
859            marker: mark,
860            held_by,
861            more,
862            branches_read,
863            pass_asked: false,
864            pass_refused: None,
865            due_at: due,
866        };
867        let span = match (taken.first(), taken.last()) {
868            (Some(first), Some(last)) => Some(Span {
869                first: *first,
870                last: *last,
871            }),
872            _ => None,
873        };
874        if pending.is_none() {
875            let Some(span) = span else {
876                return Ok(answer);
877            };
878            let decision = Decision {
879                first: span.first,
880                last: span.last,
881                count: taken.len(),
882                at: rules.now,
883            };
884            let bytes = serde_json::to_vec(&decision).map_err(Failure::storage)?;
885            self.commit_service(&[(history::decision_key(branch), Some(bytes))])?;
886            #[cfg(test)]
887            crashed(Crash::AfterDecision)?;
888        }
889        let ended = dead::ended(&self.reader, &service, branch, &taken)?;
890        self.carry_out(branch, &taken, span, rules.now, &ended, &mut answer)?;
891        Ok(answer)
892    }
893
894    /// Judge `branch`'s oldest rows from `read.from`, at most `limit` + 1 a
895    /// read (DR-13). When the first `limit` rows are all relied on by
896    /// `floor`, `judge` passes them and takes nothing, and the next release
897    /// would read the same ones. The rest of the last passed row's generation
898    /// is relied on as well (`floor_holds` is by order within a generation),
899    /// so the read starts again at the next generation. Rows are keyed by
900    /// generation, then sequence, so that start is a key, not a walk: one
901    /// more read for each generation the floor holds, and at most `limit`.
902    ///
903    /// # Errors
904    /// As [`history::oldest_rows`] and [`Self::after_newest`].
905    fn judge_from(
906        &self,
907        service: &BranchRefRecord,
908        branch: &str,
909        mut read: ReadFrom,
910        floor: &Floor,
911        rules: &Rules,
912    ) -> Result<(Vec<Row>, Judged), Failure> {
913        let cap = read.limit.get();
914        let ask = NonZeroUsize::new(cap.saturating_add(1)).unwrap_or(NonZeroUsize::MAX);
915        let mut skips = 0;
916        loop {
917            let span = (read.from, read.below.version);
918            let mut rows = history::oldest_rows(&self.reader, service, branch, span, ask)?;
919            if rows.len() <= cap {
920                let newest = rows.last();
921                rows.extend(self.after_newest(service, branch, newest, read.below, read.live)?);
922            }
923            let judged = judge(branch, &rows, cap, floor, read.below, rules);
924            match past_held(&rows, cap, floor, read.below.version) {
925                Some(from) if skips < cap => {
926                    read.from = from;
927                    skips += 1;
928                }
929                _ => return Ok((rows, judged)),
930            }
931        }
932    }
933
934    /// A read that reached `below` knows when its `newest` row was superseded:
935    /// a discarded generation's at its discard, a live branch's when the
936    /// record's next version was installed, the first it keeps after `newest`
937    /// or else the head; no read
938    /// (docs/design/HAEM-STALLED-BRANCH-NEWEST-ROW-2026-10-06.md). It is a row
939    /// at `below`, which a release never takes. A live row of an older
940    /// generation was superseded at that generation's discard, read from its
941    /// marker, one entry on each shard (DR-6); with no marker it is unknown,
942    /// so kept.
943    ///
944    /// # Errors
945    /// As [`dead::marker`].
946    fn after_newest(
947        &self,
948        service: &BranchRefRecord,
949        branch: &str,
950        newest: Option<&Row>,
951        below: Below,
952        live: bool,
953    ) -> Result<Option<Row>, Failure> {
954        let written = match newest {
955            Some(newest) if live && newest.key.generation != below.version.generation => {
956                let generation = newest.key.generation;
957                match dead::marker(&self.reader, service, branch, generation)? {
958                    Some(dead) => dead.discarded_at,
959                    None => return Ok(None),
960                }
961            }
962            Some(newest) if live => self
963                .refs
964                .kept(&branch_name(branch))
965                .iter()
966                .find(|kept| kept.seq > newest.key.sequence)
967                .map_or(below.written, |kept| kept.timestamp),
968            _ => below.written,
969        };
970        Ok(Some(dead::discard_row(branch, below.version, written)))
971    }
972
973    /// Where `branch`'s release stops, and the mark of that generation. A
974    /// name with no record releases one discarded generation, the discard
975    /// counted as the version that superseded its last (DR-6): of its
976    /// markers, at most `limit` and oldest first, the first with rows left
977    /// past its mark that `floor` does not hold; the oldest when there is
978    /// none. A decision left behind keeps the generation it was made for.
979    /// One or two point reads of a mark for each generation looked at.
980    fn below(
981        &self,
982        service: &BranchRefRecord,
983        branch: &str,
984        pending: Option<&Decision>,
985        floor: Option<&Floor>,
986        limit: NonZeroUsize,
987    ) -> Result<(Below, Option<Released>), Failure> {
988        if let Some(record) = self.refs.get(&branch_name(branch)) {
989            let below = Below {
990                version: version(record),
991                written: record.timestamp,
992            };
993            let generation = below.version.generation;
994            let mark = history::read_released(&self.reader, service, branch, generation)?;
995            return Ok((below, mark));
996        }
997        let markers = dead::of_name(&self.reader, service, branch, limit)?;
998        if let (None, Some(floor)) = (pending, floor)
999            && let Some(unheld) = self.first_unheld(service, branch, &markers, floor)?
1000        {
1001            return Ok(unheld);
1002        }
1003        let dead = pending
1004            .and_then(|decision| {
1005                markers
1006                    .iter()
1007                    .find(|dead| dead.last.generation == decision.last.generation)
1008            })
1009            .or_else(|| markers.first())
1010            .ok_or_else(|| Failure::new("branch_missing", branch))?;
1011        let generation = dead.last.generation;
1012        let mark = history::read_released(&self.reader, service, branch, generation)?;
1013        Ok((ending(dead), mark))
1014    }
1015
1016    /// The first of `markers` with rows left past its generation's mark whose
1017    /// next row is below what `floor` relies on in that generation, or which
1018    /// nothing relies on, with its mark: one or two point reads each. A
1019    /// generation a mark from before DR-6 already covers has none left
1020    /// (DR-6b).
1021    fn first_unheld(
1022        &self,
1023        service: &BranchRefRecord,
1024        branch: &str,
1025        markers: &[dead::Dead],
1026        floor: &Floor,
1027    ) -> Result<Option<(Below, Option<Released>)>, Failure> {
1028        for dead in markers {
1029            let below = ending(dead);
1030            let generation = dead.last.generation;
1031            let mark = history::read_released(&self.reader, service, branch, generation)?;
1032            let from = past_mark(mark, below.version);
1033            if order(from) >= order(below.version) {
1034                continue;
1035            }
1036            let held = floor
1037                .oldest_in(generation)
1038                .is_some_and(|relied| order(relied) <= order(from));
1039            if !held {
1040                return Ok(Some((below, mark)));
1041            }
1042        }
1043        Ok(None)
1044    }
1045
1046    /// DR-11: writes the marker `request` names in one service commit: its
1047    /// last version the generation's newest row, discarded at `request.at`.
1048    /// Refused by name: `branch_live` while the name has a record (its
1049    /// release reads every generation), `generation_marked` when the
1050    /// generation has its marker, `generation_empty` when it has no rows.
1051    /// Reads the generation's rows once, a page at a time: an operator's one
1052    /// request, never a release's wake. Answers the marker's last version.
1053    ///
1054    /// # Errors
1055    /// As above; `history_row_corrupt`, `dead_generation_corrupt`; storage
1056    /// failures.
1057    pub fn date_undated_generation(
1058        &mut self,
1059        request: &UndatedGeneration,
1060    ) -> Result<Version, Failure> {
1061        let branch = request.branch.as_str();
1062        if self.refs.get(&branch_name(branch)).is_some() {
1063            return Err(Failure::new(
1064                "branch_live",
1065                format!("{branch} has a record"),
1066            ));
1067        }
1068        let service = self
1069            .refs
1070            .get(SERVICE)
1071            .cloned()
1072            .ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
1073        let generation = request.generation;
1074        if dead::marker(&self.reader, &service, branch, generation)?.is_some() {
1075            return Err(Failure::new(
1076                "generation_marked",
1077                format!("{branch} generation {generation} has its marker"),
1078            ));
1079        }
1080        let newest = newest_row(&self.reader, &service, branch, generation)?;
1081        let Some(last) = newest else {
1082            return Err(Failure::new(
1083                "generation_empty",
1084                format!("{branch} generation {generation} has no rows"),
1085            ));
1086        };
1087        let dead = dead::Dead {
1088            branch: branch.to_owned(),
1089            last,
1090            discarded_at: request.at,
1091        };
1092        let bytes = serde_json::to_vec(&dead).map_err(Failure::storage)?;
1093        self.commit_service(&[(dead::dead_key(branch, generation), Some(bytes))])?;
1094        Ok(last)
1095    }
1096
1097    /// DR-7 (candidate B, docs/design/HAEM-RELEASE-REMAINING-RULES-2026-10-06.md):
1098    /// the heads `branch`'s record keeps beyond its newest `keep.versions`
1099    /// whose next version was installed `keep.days` ago leave the record, in
1100    /// two durable operations in this order: their rows, in one service
1101    /// commit, as an advance writes them for the heads it drops; then one
1102    /// install of the record keeping the rest (`trim_kept`). A crash between
1103    /// the two leaves the rows and the heads still kept: the next release
1104    /// finds the rows, writes none, and installs the trim. The rows are then
1105    /// released by the same rules as any other, from the next release on.
1106    /// Nothing is written while no head is due, and nothing without both
1107    /// settings. Answers when the next kept head falls due, for the releaser
1108    /// to wake then: no advance wakes a branch that stopped.
1109    fn trim_past_days(
1110        &mut self,
1111        branch: &str,
1112        rules: &Rules,
1113    ) -> Result<Option<Timestamp>, Failure> {
1114        let Some((versions, days)) = rules.keep.and_then(|keep| keep.versions.zip(keep.days))
1115        else {
1116            return Ok(None);
1117        };
1118        let name = branch_name(branch);
1119        let Some(record) = self.refs.get(&name).cloned() else {
1120            return Ok(None);
1121        };
1122        let kept = self.refs.kept(&name);
1123        if let Some(keep_from) = past_days(&record, kept, versions, days, rules.now) {
1124            self.rows_before_trim(branch, &record, keep_from)?;
1125            #[cfg(test)]
1126            crashed(Crash::BeforeTrim)?;
1127            self.refs
1128                .trim_kept(&name, record.created, record.seq, keep_from)
1129                .map_err(|error| Failure::new("release_kept_untrimmed", error.to_string()))?;
1130            // The catalogue's readers see the record keep fewer from here on.
1131            self.publish_record(&name)?;
1132        }
1133        Ok(kept_due(&record, self.refs.kept(&name), versions, days))
1134    }
1135
1136    /// A decided release's last two durable operations, in order: the files
1137    /// of `taken` (absent ones included), then its rows, mark and decision in
1138    /// one commit; then the reclaim pass asked for, a refusal named in
1139    /// `answer`, never a failure of the release.
1140    fn carry_out(
1141        &mut self,
1142        branch: &str,
1143        taken: &[Version],
1144        span: Option<Span>,
1145        now: Timestamp,
1146        ended: &[Vec<u8>],
1147        answer: &mut Answer,
1148    ) -> Result<(), Failure> {
1149        let files: Vec<String> = taken
1150            .iter()
1151            .map(|at| history::file_name(branch, *at))
1152            .collect();
1153        #[cfg(test)]
1154        if CRASH.with(|crash| crash.get() == Some(Crash::DuringFiles)) {
1155            let half = files.len() / 2;
1156            self.remove_files(files.get(..half).unwrap_or_default())?;
1157            crashed(Crash::DuringFiles)?;
1158        }
1159        self.remove_files(&files)?;
1160        #[cfg(test)]
1161        crashed(Crash::AfterFiles)?;
1162        let mut writes: Vec<(Vec<u8>, Option<Vec<u8>>)> = taken
1163            .iter()
1164            .map(|at| (history::row_key(branch, *at), None))
1165            .collect();
1166        writes.push((history::decision_key(branch), None));
1167        // A discarded generation's marker goes with its last row.
1168        writes.extend(ended.iter().map(|key| (key.clone(), None)));
1169        // DR-6: each generation taken is marked at its last version taken,
1170        // in the same commit as its rows; the name's old mark is not written.
1171        for last in last_of_each_generation(taken) {
1172            let released = Released {
1173                version: last,
1174                at: now,
1175            };
1176            let bytes = serde_json::to_vec(&released).map_err(Failure::storage)?;
1177            writes.push((
1178                history::released_key_at(branch, last.generation),
1179                Some(bytes),
1180            ));
1181        }
1182        if let Some(span) = span {
1183            answer.marker = Some(Released {
1184                version: span.last,
1185                at: now,
1186            });
1187        }
1188        self.commit_service(&writes)?;
1189        answer.released = span;
1190        #[cfg(test)]
1191        crashed(Crash::AfterCommit)?;
1192        // The release is durable by now: a pass refused is said by name in
1193        // the answer, never a failure of the release.
1194        match self.writer.request_reclaim() {
1195            Ok(()) => answer.pass_asked = true,
1196            Err(error) => answer.pass_refused = Some(error.to_string()),
1197        }
1198        Ok(())
1199    }
1200
1201    /// Remove the older history files `names` in one ref operation, absent
1202    /// ones included.
1203    fn remove_files(&mut self, names: &[String]) -> Result<(), Failure> {
1204        let removed = self
1205            .refs
1206            .remove_set(names.iter().map(String::as_str))
1207            .map_err(|error| Failure::new("release_files_unremoved", error.to_string()))?;
1208        for record in removed.removed {
1209            self.visible.remove(&record.name)?;
1210        }
1211        Ok(())
1212    }
1213
1214    /// One durable service commit of `writes`: a value puts, `None` deletes.
1215    /// A commit that failed may or may not have installed, so what the kept
1216    /// rows know is checked again, as for every service commit of rows.
1217    fn commit_service(&mut self, writes: &[(Vec<u8>, Option<Vec<u8>>)]) -> Result<(), Failure> {
1218        let committed = self.try_commit_service(writes);
1219        if committed.is_err() {
1220            self.kept_rows.mark_unsure();
1221        }
1222        committed
1223    }
1224
1225    fn try_commit_service(&mut self, writes: &[(Vec<u8>, Option<Vec<u8>>)]) -> Result<(), Failure> {
1226        let at = self
1227            .refs
1228            .get(SERVICE)
1229            .map(version)
1230            .ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
1231        let service =
1232            open_branch(SERVICE, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
1233        for (key, value) in writes {
1234            let shard = self.writer.shard_for(key);
1235            value
1236                .as_ref()
1237                .map_or_else(
1238                    || service.delete(shard, key),
1239                    |value| service.put(shard, key, value),
1240                )
1241                .map_err(Failure::branch)?;
1242        }
1243        self.writer
1244            .commit(
1245                &service,
1246                &self.registry,
1247                CommitRequest {
1248                    durability: CommitDurability::Durable {
1249                        refs: &mut self.refs,
1250                    },
1251                    extra_parents: &[],
1252                    timestamp: haematite::branch::current_timestamp(),
1253                },
1254            )
1255            .map_err(|error| {
1256                let installed = self.refs.get(SERVICE).is_some_and(|now| version(now) != at);
1257                Failure::new(
1258                    if installed {
1259                        "durability_unknown"
1260                    } else {
1261                        "write_refused"
1262                    },
1263                    error.to_string(),
1264                )
1265            })?;
1266        self.publish_record(SERVICE)
1267    }
1268}
1269
1270/// Where a discarded generation's release starts, the release stopping at
1271/// `below` (DR-6): past its own mark, else at its first version. A mark from
1272/// before DR-6 at a later generation covers this one whole, so the answer is
1273/// `below` and the range is empty, never inverted (DR-6b).
1274const fn past_mark(mark: Option<Released>, below: Version) -> Version {
1275    match mark {
1276        Some(mark) if mark.version.generation > below.generation => below,
1277        Some(mark) => after(mark),
1278        None => Version {
1279            generation: below.generation,
1280            sequence: 0,
1281        },
1282    }
1283}
1284
1285/// The newest row of `branch`'s `generation`, read oldest first a page at a
1286/// time (DR-11).
1287fn newest_row(
1288    reader: &ReadOnlyDatabase,
1289    service: &BranchRefRecord,
1290    branch: &str,
1291    generation: u64,
1292) -> Result<Option<Version>, Failure> {
1293    let page = NonZeroUsize::new(UNDATED_PAGE).unwrap_or(NonZeroUsize::MIN);
1294    let end = Version {
1295        generation,
1296        sequence: u64::MAX,
1297    };
1298    let mut from = Version {
1299        generation,
1300        sequence: 0,
1301    };
1302    let mut newest = None;
1303    loop {
1304        let rows = history::oldest_rows(reader, service, branch, (from, end), page)?;
1305        let Some(last) = rows.last().map(row_version) else {
1306            return Ok(newest);
1307        };
1308        newest = Some(last);
1309        if rows.len() < page.get() {
1310            return Ok(newest);
1311        }
1312        from = Version {
1313            generation,
1314            sequence: last.sequence.saturating_add(1),
1315        };
1316    }
1317}
1318
1319/// The first seq `record` keeps on once DR-7's trim at `now` is done: the
1320/// kept heads below it are beyond the newest `versions`, the head counted,
1321/// and were superseded `days` ago or more, by the next kept head's install
1322/// or, for the newest, the head's. `None` when no head is.
1323fn past_days(
1324    record: &BranchRefRecord,
1325    kept: &[KeptHead],
1326    versions: NonZeroU64,
1327    days: u64,
1328    now: Timestamp,
1329) -> Option<u64> {
1330    let age = days.saturating_mul(NANOS_A_DAY);
1331    let superseded = kept
1332        .iter()
1333        .skip(1)
1334        .map(|next| next.timestamp)
1335        .chain(std::iter::once(record.timestamp));
1336    let leaving = kept
1337        .iter()
1338        .zip(superseded)
1339        .take_while(|(entry, superseded)| {
1340            record.seq.saturating_sub(entry.seq) >= versions.get()
1341                && now.saturating_sub(*superseded) >= age
1342        })
1343        .count();
1344    let keep_from = kept.get(leaving).map_or(record.seq, |entry| entry.seq);
1345    (leaving > 0).then_some(keep_from)
1346}
1347
1348/// When the oldest head `record` keeps beyond its newest `versions` falls
1349/// due under `days`: its next version's install, plus the days.
1350fn kept_due(
1351    record: &BranchRefRecord,
1352    kept: &[KeptHead],
1353    versions: NonZeroU64,
1354    days: u64,
1355) -> Option<Timestamp> {
1356    let index = kept
1357        .iter()
1358        .position(|entry| record.seq.saturating_sub(entry.seq) >= versions.get())?;
1359    let superseded = kept
1360        .get(index + 1)
1361        .map_or(record.timestamp, |next| next.timestamp);
1362    Some(superseded.saturating_add(days.saturating_mul(NANOS_A_DAY)))
1363}
1364
1365/// Where a release of `dead`'s generation stops: past its last version,
1366/// superseded when it was discarded.
1367const fn ending(dead: &dead::Dead) -> Below {
1368    Below {
1369        version: Version {
1370            generation: dead.last.generation,
1371            sequence: dead.last.sequence.saturating_add(1),
1372        },
1373        written: dead.discarded_at,
1374    }
1375}
1376
1377/// The last version of each generation in `taken`, which runs oldest first.
1378fn last_of_each_generation(taken: &[Version]) -> Vec<Version> {
1379    let mut last: Vec<Version> = Vec::new();
1380    for at in taken {
1381        match last.last_mut() {
1382            Some(previous) if previous.generation == at.generation => *previous = *at,
1383            _ => last.push(*at),
1384        }
1385    }
1386    last
1387}
1388
1389/// The version just after `released`, where the next release starts.
1390const fn after(released: Released) -> Version {
1391    Version {
1392        generation: released.version.generation,
1393        sequence: released.version.sequence.saturating_add(1),
1394    }
1395}
1396
1397#[cfg(test)]
1398#[path = "release_tests.rs"]
1399mod tests;