use std::collections::BTreeMap;
use std::num::{NonZeroU64, NonZeroUsize};
use haematite::SyncCounts;
use haematite::branch::history::Row;
use haematite::branch::{CommitDurability, CommitRequest, KeptHead, Timestamp};
use haematite::db::ReadOnlyDatabase;
use haematite::tree::Hash;
use serde::Serialize;
use super::branches::provenance;
use super::dead;
use super::embedded;
use super::history::{self, Decision, Released};
use super::{
BRANCH_PREFIX, BranchRefRecord, Captured, Failure, SERVICE, Storage, branch_name, identifier,
open_branch, version,
};
use crate::serve::protocol::Version;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
Head,
Fork,
Merge,
Discard,
Embed,
EmbedOf,
KeepVersions,
KeepDays,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct HeldBy {
pub source: Source,
pub name: String,
pub version: Version,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Floor {
pub held_by: Vec<HeldBy>,
pub branches_read: usize,
}
impl Floor {
pub fn oldest(&self) -> Option<&HeldBy> {
self.held_by.first()
}
pub fn oldest_in(&self, generation: u64) -> Option<Version> {
self.held_by
.iter()
.map(|held| held.version)
.filter(|version| version.generation == generation)
.min_by_key(|version| order(*version))
}
}
pub struct IntentListing;
impl PendingDiscards for IntentListing {
fn pending(&self, storage: &Storage) -> Result<Vec<(String, Version)>, Failure> {
storage.pending_discards()
}
}
pub trait PendingDiscards {
fn pending(&self, storage: &Storage) -> Result<Vec<(String, Version)>, Failure>;
}
#[cfg(test)]
pub struct DiscardsUnread;
#[cfg(test)]
impl PendingDiscards for DiscardsUnread {
fn pending(&self, _storage: &Storage) -> Result<Vec<(String, Version)>, Failure> {
Err(Failure::new(
"release_discards_unread",
"the pending discards cannot be read yet, so nothing is released",
))
}
}
impl Storage {
pub fn relied_on(
&self,
branch: &str,
discards: &dyn PendingDiscards,
) -> Result<Floor, Failure> {
identifier(branch)?;
let head = self
.refs
.get(&branch_name(branch))
.ok_or_else(|| Failure::new("branch_missing", branch))?;
let head = HeldBy {
source: Source::Head,
name: branch.to_owned(),
version: version(head),
};
let mut floor = self.relied_on_by_others(branch, false, discards)?;
floor.held_by.push(head);
floor
.held_by
.sort_by_key(|held| (held.version.generation, held.version.sequence));
Ok(floor)
}
pub(super) fn relied_on_by_others(
&self,
branch: &str,
skip_own_discard: bool,
discards: &dyn PendingDiscards,
) -> Result<Floor, Failure> {
identifier(branch)?;
let mut held_by = Vec::new();
let mut branches_read = 0_usize;
for record in self.refs.list_prefix(BRANCH_PREFIX) {
let Some(fork) = record.name.strip_prefix(BRANCH_PREFIX) else {
continue;
};
if fork == branch {
continue;
}
let at = Captured {
branch: fork.to_owned(),
version: version(record),
shards: record.shards.clone(),
};
branches_read = branches_read.saturating_add(1);
if let Some(origin) = provenance(&self.reader, &at)?
&& origin.branch == branch
{
held_by.push(HeldBy {
source: Source::Fork,
name: fork.to_owned(),
version: origin.version,
});
}
}
for plan in self.open_merge_plans()? {
for pin in [&plan.origin, &plan.source, &plan.target] {
if pin.branch == branch {
held_by.push(HeldBy {
source: Source::Merge,
name: plan.id.clone(),
version: pin.version,
});
}
}
}
for (discarded, at) in discards.pending(self)? {
if discarded == branch && !skip_own_discard {
held_by.push(HeldBy {
source: Source::Discard,
name: discarded,
version: at,
});
}
}
held_by.extend(self.embed_positions(branch)?);
held_by.sort_by_key(|held| (held.version.generation, held.version.sequence));
Ok(Floor {
held_by,
branches_read,
})
}
}
impl Storage {
fn embed_positions(&self, branch: &str) -> Result<Vec<HeldBy>, Failure> {
let mut held = Vec::new();
for record in self.refs.list_prefix(embedded::COMPANION_PREFIX) {
let Some(owner) = record.name.strip_prefix(embedded::COMPANION_PREFIX) else {
continue;
};
for ((collection, field), position) in self.companion_positions(record)? {
let field = format!("{collection}.{field}");
if position.branch == branch
&& let Some(through) = position.through
{
let (source, name) = if owner == branch {
(Source::Embed, field.clone())
} else {
(Source::EmbedOf, format!("{owner}:{field}"))
};
held.push(HeldBy {
source,
name,
version: through,
});
}
if owner == branch
&& let Some(open) = position.toward
{
held.push(HeldBy {
source: Source::Embed,
name: field,
version: open.version,
});
}
}
}
Ok(held)
}
pub(super) fn companion_positions(
&self,
companion: &BranchRefRecord,
) -> Result<BTreeMap<(String, String), embedded::Position>, Failure> {
let limit = NonZeroUsize::new(POSITIONS_A_PAGE)
.ok_or_else(|| Failure::new("embed_positions_page", "a page must be nonzero"))?;
let mut positions = BTreeMap::new();
for shard in &companion.shards {
let mut from = b"e\0".to_vec();
loop {
let page = self
.reader
.range_page_at(shard.shard_id, shard.head, &from, b"e\x01", limit)
.map_err(Failure::observer)?;
for (key, bytes) in page.entries {
let field = position_field(&companion.name, &key)?;
positions.insert(field, embedded::read_position(&bytes)?);
}
let Some(next) = page.next_from else {
break;
};
from = next;
}
}
Ok(positions)
}
}
fn position_field(companion: &str, key: &[u8]) -> Result<(String, String), Failure> {
let corrupt = || {
Failure::new(
"embed_position_key_corrupt",
format!("{companion} holds a position key that names no collection and field"),
)
};
let named = key.strip_prefix(b"e\0".as_slice()).ok_or_else(corrupt)?;
let named = std::str::from_utf8(named).map_err(|_error| corrupt())?;
let (collection, field) = named.split_once('\0').ok_or_else(corrupt)?;
Ok((collection.to_owned(), field.to_owned()))
}
const POSITIONS_A_PAGE: usize = 64;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Keep {
pub versions: Option<NonZeroU64>,
pub days: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rules {
pub keep: Option<Keep>,
pub max_rows: NonZeroUsize,
pub now: Timestamp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct Span {
pub first: Version,
pub last: Version,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Answer {
pub branch: String,
pub released: Option<Span>,
pub marker: Option<Released>,
pub held_by: Vec<HeldBy>,
pub more: bool,
pub branches_read: usize,
pub pass_asked: bool,
pub pass_refused: Option<String>,
pub due_at: Option<Timestamp>,
}
const NANOS_A_DAY: u64 = 86_400 * 1_000_000_000;
const LOWEST: Version = Version {
generation: 0,
sequence: 0,
};
const fn order(version: Version) -> (u64, u64) {
(version.generation, version.sequence)
}
const fn row_version(row: &Row) -> Version {
Version {
generation: row.key.generation,
sequence: row.key.sequence,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Below {
pub version: Version,
pub written: Timestamp,
}
fn judge(
branch: &str,
rows: &[Row],
cap: usize,
floor: &Floor,
below: Below,
rules: &Rules,
) -> (Vec<Version>, Vec<HeldBy>, bool) {
let mut taken = Vec::new();
let mut passed = Vec::new();
for (index, row) in rows.iter().enumerate() {
let at = row_version(row);
if order(at) >= order(below.version) {
break;
}
let held = held_at(branch, rows, index, floor, below, rules);
if index == cap {
let more = held.is_empty() && !taken.is_empty();
let held_by = if taken.is_empty() { passed } else { Vec::new() };
return (taken, held_by, more);
}
if !held.is_empty() {
if taken.is_empty() && floor_holds(floor, at) {
if passed.is_empty() {
passed = held;
}
continue;
}
return (taken, held, false);
}
taken.push(at);
}
let held_by = if taken.is_empty() { passed } else { Vec::new() };
(taken, held_by, false)
}
#[derive(Debug, Clone, Copy)]
struct ReadFrom {
from: Version,
below: Below,
limit: NonZeroUsize,
live: bool,
}
type Judged = (Vec<Version>, Vec<HeldBy>, bool);
fn past_held(rows: &[Row], cap: usize, floor: &Floor, below: Version) -> Option<Version> {
if rows.len() <= cap {
return None;
}
let passed = rows.get(..cap)?;
if !passed
.iter()
.all(|row| floor_holds(floor, row_version(row)))
{
return None;
}
let generation = passed.last()?.key.generation;
(generation < below.generation).then(|| Version {
generation: generation.saturating_add(1),
sequence: 0,
})
}
fn floor_holds(floor: &Floor, at: Version) -> bool {
let generation = at.generation;
floor
.oldest_in(generation)
.is_some_and(|oldest| order(at) >= order(oldest))
}
fn decided_rows(
reader: &ReadOnlyDatabase,
service: &BranchRefRecord,
branch: &str,
decision: &Decision,
below: Below,
) -> Result<Vec<Version>, Failure> {
let read = NonZeroUsize::new(decision.count.saturating_add(1)).unwrap_or(NonZeroUsize::MAX);
let rows = history::oldest_rows(
reader,
service,
branch,
(decision.first, below.version),
read,
)?;
let taken: Vec<Version> = rows
.iter()
.map(row_version)
.filter(|at| order(*at) <= order(decision.last))
.collect();
if taken.len() != decision.count {
return Err(Failure::new(
"release_decision_mismatch",
format!(
"the release decided for {branch} names {} rows from {}.{} to {}.{}, and {} \
are there; the decision is left",
decision.count,
decision.first.generation,
decision.first.sequence,
decision.last.generation,
decision.last.sequence,
taken.len()
),
));
}
Ok(taken)
}
fn held_at(
branch: &str,
rows: &[Row],
index: usize,
floor: &Floor,
below: Below,
rules: &Rules,
) -> Vec<HeldBy> {
let Some(row) = rows.get(index) else {
return Vec::new();
};
let at = row_version(row);
if let Some(oldest) = floor.oldest_in(at.generation)
&& order(at) >= order(oldest)
{
return floor
.held_by
.iter()
.filter(|held| held.version == oldest)
.cloned()
.collect();
}
let Some(keep) = rules.keep else {
return Vec::new();
};
let kept = |source| {
vec![HeldBy {
source,
name: branch.to_owned(),
version: at,
}]
};
let Some(versions) = keep.versions else {
return kept(Source::KeepVersions);
};
let Some(days) = keep.days else {
return kept(Source::KeepDays);
};
let head = below.version;
let newer = newer_than(rows, index, head);
if newer < versions.get() {
return kept(Source::KeepVersions);
}
let superseded = match rows.get(index + 1) {
Some(next) => Some(next.timestamp),
None if order(at) < order(head) && rows.len() <= index + 1 => None,
None => Some(below.written),
};
let old_enough = superseded.is_some_and(|superseded| {
rules.now.saturating_sub(superseded) >= days.saturating_mul(NANOS_A_DAY)
});
if !old_enough {
return kept(Source::KeepDays);
}
Vec::new()
}
fn due_at(rows: &[Row], held_by: &[HeldBy], rules: &Rules) -> Option<Timestamp> {
let days = rules.keep?.days?;
let held = held_by
.iter()
.find(|held| held.source == Source::KeepDays)?;
let index = rows
.iter()
.position(|row| row_version(row) == held.version)?;
let superseded = rows.get(index + 1)?.timestamp;
Some(superseded.saturating_add(days.saturating_mul(NANOS_A_DAY)))
}
fn newer_than(rows: &[Row], index: usize, head: Version) -> u64 {
let Some(row) = rows.get(index) else {
return 0;
};
let at = row_version(row);
if at.generation == head.generation {
return head.sequence.saturating_sub(at.sequence);
}
let older_after = rows
.get(index + 1..)
.unwrap_or_default()
.iter()
.filter(|later| later.key.generation < head.generation)
.count();
head.sequence
.saturating_add(1)
.saturating_add(u64::try_from(older_after).unwrap_or(0))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UndatedGeneration {
pub branch: String,
pub generation: u64,
pub at: Timestamp,
}
const UNDATED_PAGE: usize = 256;
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Crash {
AfterDecision,
DuringFiles,
AfterFiles,
AfterCommit,
BeforeTrim,
}
#[cfg(test)]
thread_local! {
static CRASH: std::cell::Cell<Option<Crash>> = const { std::cell::Cell::new(None) };
}
#[cfg(test)]
pub fn crash_at(point: Crash) {
CRASH.with(|crash| crash.set(Some(point)));
}
#[cfg(test)]
fn crashed(point: Crash) -> Result<(), Failure> {
if CRASH.with(|crash| crash.get() == Some(point)) {
CRASH.with(|crash| crash.set(None));
return Err(Failure::new("release_crashed", format!("{point:?}")));
}
Ok(())
}
impl Storage {
pub fn release(
&mut self,
branch: &str,
limit: Option<NonZeroUsize>,
rules: &Rules,
discards: &dyn PendingDiscards,
) -> Result<Answer, Failure> {
let answer = self.release_once(branch, limit, rules, discards);
self.kept_rows.released(branch);
answer
}
#[must_use]
pub fn branch_names(&self) -> Vec<String> {
self.refs
.list_prefix(BRANCH_PREFIX)
.filter_map(|record| record.name.strip_prefix(BRANCH_PREFIX))
.map(str::to_owned)
.collect()
}
pub fn decided_branches(&self) -> Result<Vec<String>, Failure> {
let service = self
.refs
.get(SERVICE)
.ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
let decided = history::decided(&self.reader, service)?;
Ok(self
.refs
.list_prefix(BRANCH_PREFIX)
.filter_map(|record| record.name.strip_prefix(BRANCH_PREFIX))
.filter(|name| decided.contains(&Hash::of(name.as_bytes())))
.map(str::to_owned)
.collect())
}
#[must_use]
pub fn advanced_since_release(&self, branch: &str) -> usize {
self.kept_rows.advanced(branch)
}
#[must_use]
pub fn sync_counts(&self) -> SyncCounts {
self.writer.sync_counts()
}
fn release_once(
&mut self,
branch: &str,
limit: Option<NonZeroUsize>,
rules: &Rules,
discards: &dyn PendingDiscards,
) -> Result<Answer, Failure> {
let limit = match limit {
Some(limit) if limit > rules.max_rows => {
return Err(Failure::new(
"release_limit_exceeded",
format!(
"limit {limit} is above retention.release_max_rows {}",
rules.max_rows
),
));
}
Some(limit) => limit,
None => rules.max_rows,
};
let service = self
.refs
.get(SERVICE)
.cloned()
.ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
let live = self.refs.get(&branch_name(branch)).is_some();
let pending = history::read_decision(&self.reader, &service, branch)?;
let floor = match (&pending, live) {
(Some(_decision), _) => None,
(None, true) => Some(self.relied_on(branch, discards)?),
(None, false) => Some(self.relied_on_by_others(branch, false, discards)?),
};
let (below, mark) =
self.below(&service, branch, pending.as_ref(), floor.as_ref(), limit)?;
let (taken, held_by, more, branches_read, due) = if let Some(decision) = &pending {
let taken = decided_rows(&self.reader, &service, branch, decision, below)?;
(taken, Vec::new(), false, 0, None)
} else {
let floor = floor.unwrap_or_default();
let from = if live {
LOWEST
} else {
past_mark(mark, below.version)
};
let read = ReadFrom {
from,
below,
limit,
live,
};
let (rows, (taken, held_by, more)) =
self.judge_from(&service, branch, read, &floor, rules)?;
let mut due = due_at(&rows, &held_by, rules);
if live {
let trim_due = self.trim_past_days(branch, rules)?;
due = due.into_iter().chain(trim_due).min();
}
(taken, held_by, more, floor.branches_read, due)
};
let mut answer = Answer {
branch: branch.to_owned(),
released: None,
marker: mark,
held_by,
more,
branches_read,
pass_asked: false,
pass_refused: None,
due_at: due,
};
let span = match (taken.first(), taken.last()) {
(Some(first), Some(last)) => Some(Span {
first: *first,
last: *last,
}),
_ => None,
};
if pending.is_none() {
let Some(span) = span else {
return Ok(answer);
};
let decision = Decision {
first: span.first,
last: span.last,
count: taken.len(),
at: rules.now,
};
let bytes = serde_json::to_vec(&decision).map_err(Failure::storage)?;
self.commit_service(&[(history::decision_key(branch), Some(bytes))])?;
#[cfg(test)]
crashed(Crash::AfterDecision)?;
}
let ended = dead::ended(&self.reader, &service, branch, &taken)?;
self.carry_out(branch, &taken, span, rules.now, &ended, &mut answer)?;
Ok(answer)
}
fn judge_from(
&self,
service: &BranchRefRecord,
branch: &str,
mut read: ReadFrom,
floor: &Floor,
rules: &Rules,
) -> Result<(Vec<Row>, Judged), Failure> {
let cap = read.limit.get();
let ask = NonZeroUsize::new(cap.saturating_add(1)).unwrap_or(NonZeroUsize::MAX);
let mut skips = 0;
loop {
let span = (read.from, read.below.version);
let mut rows = history::oldest_rows(&self.reader, service, branch, span, ask)?;
if rows.len() <= cap {
let newest = rows.last();
rows.extend(self.after_newest(service, branch, newest, read.below, read.live)?);
}
let judged = judge(branch, &rows, cap, floor, read.below, rules);
match past_held(&rows, cap, floor, read.below.version) {
Some(from) if skips < cap => {
read.from = from;
skips += 1;
}
_ => return Ok((rows, judged)),
}
}
}
fn after_newest(
&self,
service: &BranchRefRecord,
branch: &str,
newest: Option<&Row>,
below: Below,
live: bool,
) -> Result<Option<Row>, Failure> {
let written = match newest {
Some(newest) if live && newest.key.generation != below.version.generation => {
let generation = newest.key.generation;
match dead::marker(&self.reader, service, branch, generation)? {
Some(dead) => dead.discarded_at,
None => return Ok(None),
}
}
Some(newest) if live => self
.refs
.kept(&branch_name(branch))
.iter()
.find(|kept| kept.seq > newest.key.sequence)
.map_or(below.written, |kept| kept.timestamp),
_ => below.written,
};
Ok(Some(dead::discard_row(branch, below.version, written)))
}
fn below(
&self,
service: &BranchRefRecord,
branch: &str,
pending: Option<&Decision>,
floor: Option<&Floor>,
limit: NonZeroUsize,
) -> Result<(Below, Option<Released>), Failure> {
if let Some(record) = self.refs.get(&branch_name(branch)) {
let below = Below {
version: version(record),
written: record.timestamp,
};
let generation = below.version.generation;
let mark = history::read_released(&self.reader, service, branch, generation)?;
return Ok((below, mark));
}
let markers = dead::of_name(&self.reader, service, branch, limit)?;
if let (None, Some(floor)) = (pending, floor)
&& let Some(unheld) = self.first_unheld(service, branch, &markers, floor)?
{
return Ok(unheld);
}
let dead = pending
.and_then(|decision| {
markers
.iter()
.find(|dead| dead.last.generation == decision.last.generation)
})
.or_else(|| markers.first())
.ok_or_else(|| Failure::new("branch_missing", branch))?;
let generation = dead.last.generation;
let mark = history::read_released(&self.reader, service, branch, generation)?;
Ok((ending(dead), mark))
}
fn first_unheld(
&self,
service: &BranchRefRecord,
branch: &str,
markers: &[dead::Dead],
floor: &Floor,
) -> Result<Option<(Below, Option<Released>)>, Failure> {
for dead in markers {
let below = ending(dead);
let generation = dead.last.generation;
let mark = history::read_released(&self.reader, service, branch, generation)?;
let from = past_mark(mark, below.version);
if order(from) >= order(below.version) {
continue;
}
let held = floor
.oldest_in(generation)
.is_some_and(|relied| order(relied) <= order(from));
if !held {
return Ok(Some((below, mark)));
}
}
Ok(None)
}
pub fn date_undated_generation(
&mut self,
request: &UndatedGeneration,
) -> Result<Version, Failure> {
let branch = request.branch.as_str();
if self.refs.get(&branch_name(branch)).is_some() {
return Err(Failure::new(
"branch_live",
format!("{branch} has a record"),
));
}
let service = self
.refs
.get(SERVICE)
.cloned()
.ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
let generation = request.generation;
if dead::marker(&self.reader, &service, branch, generation)?.is_some() {
return Err(Failure::new(
"generation_marked",
format!("{branch} generation {generation} has its marker"),
));
}
let newest = newest_row(&self.reader, &service, branch, generation)?;
let Some(last) = newest else {
return Err(Failure::new(
"generation_empty",
format!("{branch} generation {generation} has no rows"),
));
};
let dead = dead::Dead {
branch: branch.to_owned(),
last,
discarded_at: request.at,
};
let bytes = serde_json::to_vec(&dead).map_err(Failure::storage)?;
self.commit_service(&[(dead::dead_key(branch, generation), Some(bytes))])?;
Ok(last)
}
fn trim_past_days(
&mut self,
branch: &str,
rules: &Rules,
) -> Result<Option<Timestamp>, Failure> {
let Some((versions, days)) = rules.keep.and_then(|keep| keep.versions.zip(keep.days))
else {
return Ok(None);
};
let name = branch_name(branch);
let Some(record) = self.refs.get(&name).cloned() else {
return Ok(None);
};
let kept = self.refs.kept(&name);
if let Some(keep_from) = past_days(&record, kept, versions, days, rules.now) {
self.rows_before_trim(branch, &record, keep_from)?;
#[cfg(test)]
crashed(Crash::BeforeTrim)?;
self.refs
.trim_kept(&name, record.created, record.seq, keep_from)
.map_err(|error| Failure::new("release_kept_untrimmed", error.to_string()))?;
self.publish_record(&name)?;
}
Ok(kept_due(&record, self.refs.kept(&name), versions, days))
}
fn carry_out(
&mut self,
branch: &str,
taken: &[Version],
span: Option<Span>,
now: Timestamp,
ended: &[Vec<u8>],
answer: &mut Answer,
) -> Result<(), Failure> {
let files: Vec<String> = taken
.iter()
.map(|at| history::file_name(branch, *at))
.collect();
#[cfg(test)]
if CRASH.with(|crash| crash.get() == Some(Crash::DuringFiles)) {
let half = files.len() / 2;
self.remove_files(files.get(..half).unwrap_or_default())?;
crashed(Crash::DuringFiles)?;
}
self.remove_files(&files)?;
#[cfg(test)]
crashed(Crash::AfterFiles)?;
let mut writes: Vec<(Vec<u8>, Option<Vec<u8>>)> = taken
.iter()
.map(|at| (history::row_key(branch, *at), None))
.collect();
writes.push((history::decision_key(branch), None));
writes.extend(ended.iter().map(|key| (key.clone(), None)));
for last in last_of_each_generation(taken) {
let released = Released {
version: last,
at: now,
};
let bytes = serde_json::to_vec(&released).map_err(Failure::storage)?;
writes.push((
history::released_key_at(branch, last.generation),
Some(bytes),
));
}
if let Some(span) = span {
answer.marker = Some(Released {
version: span.last,
at: now,
});
}
self.commit_service(&writes)?;
answer.released = span;
#[cfg(test)]
crashed(Crash::AfterCommit)?;
match self.writer.request_reclaim() {
Ok(()) => answer.pass_asked = true,
Err(error) => answer.pass_refused = Some(error.to_string()),
}
Ok(())
}
fn remove_files(&mut self, names: &[String]) -> Result<(), Failure> {
let removed = self
.refs
.remove_set(names.iter().map(String::as_str))
.map_err(|error| Failure::new("release_files_unremoved", error.to_string()))?;
for record in removed.removed {
self.visible.remove(&record.name)?;
}
Ok(())
}
fn commit_service(&mut self, writes: &[(Vec<u8>, Option<Vec<u8>>)]) -> Result<(), Failure> {
let committed = self.try_commit_service(writes);
if committed.is_err() {
self.kept_rows.mark_unsure();
}
committed
}
fn try_commit_service(&mut self, writes: &[(Vec<u8>, Option<Vec<u8>>)]) -> Result<(), Failure> {
let at = self
.refs
.get(SERVICE)
.map(version)
.ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
let service =
open_branch(SERVICE, &self.refs, &self.registry).map_err(Failure::branch_commit)?;
for (key, value) in writes {
let shard = self.writer.shard_for(key);
value
.as_ref()
.map_or_else(
|| service.delete(shard, key),
|value| service.put(shard, key, value),
)
.map_err(Failure::branch)?;
}
self.writer
.commit(
&service,
&self.registry,
CommitRequest {
durability: CommitDurability::Durable {
refs: &mut self.refs,
},
extra_parents: &[],
timestamp: haematite::branch::current_timestamp(),
},
)
.map_err(|error| {
let installed = self.refs.get(SERVICE).is_some_and(|now| version(now) != at);
Failure::new(
if installed {
"durability_unknown"
} else {
"write_refused"
},
error.to_string(),
)
})?;
self.publish_record(SERVICE)
}
}
const fn past_mark(mark: Option<Released>, below: Version) -> Version {
match mark {
Some(mark) if mark.version.generation > below.generation => below,
Some(mark) => after(mark),
None => Version {
generation: below.generation,
sequence: 0,
},
}
}
fn newest_row(
reader: &ReadOnlyDatabase,
service: &BranchRefRecord,
branch: &str,
generation: u64,
) -> Result<Option<Version>, Failure> {
let page = NonZeroUsize::new(UNDATED_PAGE).unwrap_or(NonZeroUsize::MIN);
let end = Version {
generation,
sequence: u64::MAX,
};
let mut from = Version {
generation,
sequence: 0,
};
let mut newest = None;
loop {
let rows = history::oldest_rows(reader, service, branch, (from, end), page)?;
let Some(last) = rows.last().map(row_version) else {
return Ok(newest);
};
newest = Some(last);
if rows.len() < page.get() {
return Ok(newest);
}
from = Version {
generation,
sequence: last.sequence.saturating_add(1),
};
}
}
fn past_days(
record: &BranchRefRecord,
kept: &[KeptHead],
versions: NonZeroU64,
days: u64,
now: Timestamp,
) -> Option<u64> {
let age = days.saturating_mul(NANOS_A_DAY);
let superseded = kept
.iter()
.skip(1)
.map(|next| next.timestamp)
.chain(std::iter::once(record.timestamp));
let leaving = kept
.iter()
.zip(superseded)
.take_while(|(entry, superseded)| {
record.seq.saturating_sub(entry.seq) >= versions.get()
&& now.saturating_sub(*superseded) >= age
})
.count();
let keep_from = kept.get(leaving).map_or(record.seq, |entry| entry.seq);
(leaving > 0).then_some(keep_from)
}
fn kept_due(
record: &BranchRefRecord,
kept: &[KeptHead],
versions: NonZeroU64,
days: u64,
) -> Option<Timestamp> {
let index = kept
.iter()
.position(|entry| record.seq.saturating_sub(entry.seq) >= versions.get())?;
let superseded = kept
.get(index + 1)
.map_or(record.timestamp, |next| next.timestamp);
Some(superseded.saturating_add(days.saturating_mul(NANOS_A_DAY)))
}
const fn ending(dead: &dead::Dead) -> Below {
Below {
version: Version {
generation: dead.last.generation,
sequence: dead.last.sequence.saturating_add(1),
},
written: dead.discarded_at,
}
}
fn last_of_each_generation(taken: &[Version]) -> Vec<Version> {
let mut last: Vec<Version> = Vec::new();
for at in taken {
match last.last_mut() {
Some(previous) if previous.generation == at.generation => *previous = *at,
_ => last.push(*at),
}
}
last
}
const fn after(released: Released) -> Version {
Version {
generation: released.version.generation,
sequence: released.version.sequence.saturating_add(1),
}
}
#[cfg(test)]
#[path = "release_tests.rs"]
mod tests;