use std::num::NonZeroUsize;
use haematite::branch::current_timestamp;
use haematite::db::ReadOnlyDatabase;
use serde::{Deserialize, Serialize};
use super::branches::{self, ForkProvenance};
use super::mutations::PreparedWrite;
use super::release::{Answer, Rules};
use super::{Captured, Failure, SERVICE, Storage, branch_name, dead, history, identifier, reads};
use crate::serve::protocol::{Change, Stage, Version};
pub const TAG: &[u8] = b"s\0";
const FORMAT: u32 = 1;
const RELEASE_ROWS: usize = 4_096;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct At {
pub branch: String,
pub version: Version,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Listed {
pub branch: String,
pub version: Version,
pub stage: Stage,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Merged {
pub from: At,
pub stages: Vec<Listed>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Row {
format: u32,
version: Version,
#[serde(default, skip_serializing_if = "Option::is_none")]
stage: Option<Stage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
merged: Option<Merged>,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Lineage {
pub stage: Option<Stage>,
pub merged: Option<Merged>,
}
#[must_use]
pub fn key(version: Version) -> Vec<u8> {
let mut key = Vec::with_capacity(TAG.len() + 16);
key.extend_from_slice(TAG);
key.extend_from_slice(&version.generation.to_be_bytes());
key.extend_from_slice(&version.sequence.to_be_bytes());
key
}
fn key_version(key: &[u8]) -> Result<Version, Failure> {
let corrupt = || Failure::new("stage_row_corrupt", "stage row key is not a version");
let rest = key.strip_prefix(TAG).ok_or_else(corrupt)?;
let (generation, sequence) = rest.split_at_checked(8).ok_or_else(corrupt)?;
Ok(Version {
generation: u64::from_be_bytes(generation.try_into().map_err(|_| corrupt())?),
sequence: u64::from_be_bytes(sequence.try_into().map_err(|_| corrupt())?),
})
}
fn named(value: &str, path: String) -> Result<(), Failure> {
identifier(value).map_err(|mut failure| {
failure.message = format!("{path}: {}", failure.message);
failure.path = Some(path);
failure
})
}
pub(super) fn validate(stage: &Stage, change: &Change) -> Result<(), Failure> {
if matches!(
change,
Change::Fork(_) | Change::Discard(_) | Change::Merge(_)
) {
return Err(Failure {
code: "invalid_operation".to_owned(),
message: "a stage is written by a data change: branch.fork, branch.discard and \
branch.merge take none"
.to_owned(),
path: Some("/stage".to_owned()),
cause: None,
});
}
named(&stage.line, "/stage/line".to_owned())?;
named(&stage.run, "/stage/run".to_owned())?;
named(&stage.stage, "/stage/stage".to_owned())?;
named(&stage.subject, "/stage/subject".to_owned())?;
for (index, input) in stage.inputs.iter().enumerate() {
named(&input.branch, format!("/stage/inputs/{index}/branch"))?;
}
Ok(())
}
fn encode(row: &Row) -> Result<Vec<u8>, Failure> {
serde_json::to_vec(row).map_err(Failure::storage)
}
pub(super) fn stage_write(next: Version, stage: &Stage) -> Result<PreparedWrite, Failure> {
let row = Row {
format: FORMAT,
version: next,
stage: Some(stage.clone()),
merged: None,
};
Ok((key(next), Some(encode(&row)?)))
}
fn decode(key: &[u8], bytes: &[u8]) -> Result<Row, Failure> {
let row: Row = serde_json::from_slice(bytes)
.map_err(|error| Failure::new("stage_row_corrupt", error.to_string()))?;
if row.format != FORMAT {
return Err(Failure::new(
"stage_row_format_unsupported",
format!("stage row format {} is not {FORMAT}", row.format),
));
}
if row.version != key_version(key)? || row.stage.is_some() == row.merged.is_some() {
return Err(Failure::new(
"stage_row_corrupt",
"a stage row names its own version and holds a stage or a merge, not both",
));
}
Ok(row)
}
#[derive(Debug, Default)]
pub(super) struct Found(Vec<(Version, Vec<Listed>)>);
impl Found {
pub(super) fn add(&mut self, source: &str, key: &[u8], bytes: &[u8]) -> Result<(), Failure> {
let row = decode(key, bytes)?;
let listed = match (row.stage, row.merged) {
(Some(stage), _) => vec![Listed {
branch: source.to_owned(),
version: row.version,
stage,
}],
(None, Some(merged)) => merged.stages,
(None, None) => Vec::new(),
};
self.0.push((row.version, listed));
Ok(())
}
const fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn write(
mut self,
next: Version,
from: &str,
version: Version,
) -> Result<PreparedWrite, Failure> {
self.0
.sort_unstable_by_key(|(at, _)| (at.generation, at.sequence));
let row = Row {
format: FORMAT,
version: next,
stage: None,
merged: Some(Merged {
from: At {
branch: from.to_owned(),
version,
},
stages: self.0.into_iter().flat_map(|(_, listed)| listed).collect(),
}),
};
Ok((key(next), Some(encode(&row)?)))
}
}
pub(super) fn merged_write(
reader: &ReadOnlyDatabase,
source: &Captured,
found: Found,
next: Version,
) -> Result<Option<PreparedWrite>, Failure> {
if found.is_empty() && !is_line(reader, source)? {
return Ok(None);
}
found.write(next, &source.branch, source.version).map(Some)
}
pub fn read(reader: &ReadOnlyDatabase, at: &Captured) -> Result<Lineage, Failure> {
let key = key(at.version);
let Some(bytes) = reads::get_bytes(reader, at, &key)? else {
return Ok(Lineage::default());
};
let row = decode(&key, &bytes)?;
Ok(Lineage {
stage: row.stage,
merged: row.merged,
})
}
pub(super) fn is_line(reader: &ReadOnlyDatabase, at: &Captured) -> Result<bool, Failure> {
Ok(branches::origin(reader, at)?.is_some_and(|(_origin, line)| line))
}
impl Storage {
pub(super) fn release_after_discard(
&mut self,
branch: &str,
generation: u64,
) -> Result<(), Failure> {
self.release_lines(branch, generation).map_err(|failure| {
Failure::new(
"line_release_unfinished",
format!(
"the discard of {branch} is durable; releasing its line versions stopped: \
{failure}"
),
)
})
}
fn release_lines(&mut self, branch: &str, generation: u64) -> Result<(), Failure> {
let Some((origin, line)) = self.discarded_origin(branch, generation)? else {
return Ok(());
};
let parent = origin.version.generation;
if self.refs.get(&branch_name(&origin.branch)).is_none()
&& let Some((_, true)) = self.discarded_origin(&origin.branch, parent)?
{
self.release_line(&origin.branch, parent)?;
}
if line {
self.release_line(branch, generation)?;
}
Ok(())
}
fn discarded_origin(
&self,
branch: &str,
generation: u64,
) -> Result<Option<(ForkProvenance, bool)>, Failure> {
let service = self
.refs
.get(SERVICE)
.ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
let Some(marker) = dead::marker(&self.reader, service, branch, generation)? else {
return Ok(None);
};
let Some(last) = history::capture_kept(&self.reader, service, branch, marker.last)? else {
return Ok(None);
};
branches::origin(&self.reader, &last)
}
fn release_line(&mut self, branch: &str, generation: u64) -> Result<(), Failure> {
if self.refs.get(&branch_name(branch)).is_some() {
return Ok(());
}
let max_rows = NonZeroUsize::new(RELEASE_ROWS).unwrap_or(NonZeroUsize::MIN);
loop {
let service = self
.refs
.get(SERVICE)
.ok_or_else(|| Failure::new("reference_missing", "missing service namespace"))?;
if dead::marker(&self.reader, service, branch, generation)?.is_none() {
return Ok(());
}
let rules = Rules {
keep: None,
max_rows,
now: current_timestamp(),
};
let answer: Answer = self.release_generation(branch, generation, &rules)?;
if answer.released.is_none() || !answer.more {
return Ok(());
}
}
}
}
#[cfg(test)]
pub mod before_commit {
use std::cell::Cell;
use super::Failure;
thread_local! {
static ARMED: Cell<bool> = const { Cell::new(false) };
}
pub fn arm() {
ARMED.with(|armed| armed.set(true));
}
pub fn reached() -> Result<(), Failure> {
if ARMED.with(|armed| armed.replace(false)) {
return Err(Failure::new(
"injected_crash",
"stopped after the writes were staged and before the commit",
));
}
Ok(())
}
}