use std::collections::BTreeSet;
use std::fmt;
use haematite::tree::Hash;
use liminal::durability::bridge::block_on;
use liminal::durability::{DurableStore, RoutedBatchItem};
use super::log::OperationLogError;
use super::operation_batch::OperationBatch;
use super::registry::{REGISTRY_STREAM_KEY, SCHEMA_VERSION, SCHEMA_VERSION_V1};
const SNAPSHOT_KEY_TEXT: &str = "registry/snapshot/v1";
pub(super) const SNAPSHOT_KEY: &[u8] = SNAPSHOT_KEY_TEXT.as_bytes();
const MAGIC: [u8; 4] = *b"LSRS";
const VERSION: (u16, u16) = (2, 0);
const VERSION_V1: (u16, u16) = (1, 0);
pub(super) fn snapshot_item(
covered_next_seq: u64,
sets: &RegistrySets,
expected: Option<Hash>,
) -> Result<(RoutedBatchItem, Hash), OperationLogError> {
let value = RegistrySnapshot::encode(covered_next_seq, sets)
.map_err(OperationLogError::RegistrySnapshot)?;
let hash = Hash::of(&value);
Ok((
RoutedBatchItem {
key: SNAPSHOT_KEY.to_vec(),
expected,
value,
ttl: None,
},
hash,
))
}
pub(super) fn write_registry_snapshot(
store: &dyn DurableStore,
expected: Option<Hash>,
covered_next_seq: u64,
sets: &RegistrySets,
) -> Result<Hash, OperationLogError> {
let (item, hash) = snapshot_item(covered_next_seq, sets, expected)?;
let mut batch = OperationBatch::new(REGISTRY_STREAM_KEY);
batch.stage(item)?;
block_on(batch.commit(store, || {}))??;
Ok(hash)
}
const DIGEST_DOMAIN: &[u8] = b"liminal-registry-snapshot-v2";
const DIGEST_DOMAIN_V1: &[u8] = b"liminal-registry-snapshot-v1";
const HEADER_BYTES: usize = 32;
const DIGEST_BYTES: usize = 32;
const EMPTY_BYTES: usize = HEADER_BYTES + DIGEST_BYTES;
const ID_BYTES: usize = 8;
const PADDING_BYTES: usize = 7;
const ROW_SCHEMA_AT: usize = 8;
const PADDING_AT: usize = 9;
const COVERED_AT: usize = 16;
const COUNT_AT: usize = 24;
const SWEEPING_COUNT_BYTES: usize = 8;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct RegistrySets {
pub(super) live: BTreeSet<u64>,
pub(super) sweeping: BTreeSet<u64>,
}
#[cfg(test)]
impl RegistrySets {
pub(super) fn of_live(live: BTreeSet<u64>) -> Self {
Self {
live,
sweeping: BTreeSet::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct RegistrySnapshot {
pub(super) covered_next_seq: u64,
pub(super) sets: RegistrySets,
}
impl RegistrySnapshot {
pub(super) fn encode(
covered_next_seq: u64,
sets: &RegistrySets,
) -> Result<Vec<u8>, RegistrySnapshotError> {
if let Some(id) = sets.live.intersection(&sets.sweeping).next() {
return Err(RegistrySnapshotError::BothSets { id: *id });
}
let ids = sets.live.len().saturating_add(sets.sweeping.len());
let too_large = RegistrySnapshotError::TooLarge { count: ids };
let live = u64::try_from(sets.live.len()).map_err(|_| too_large.clone())?;
let sweeping = u64::try_from(sets.sweeping.len()).map_err(|_| too_large.clone())?;
let length = ids
.checked_mul(ID_BYTES)
.and_then(|bytes| bytes.checked_add(EMPTY_BYTES + SWEEPING_COUNT_BYTES))
.ok_or(too_large)?;
let mut bytes = Vec::with_capacity(length);
bytes.extend_from_slice(&MAGIC);
bytes.extend_from_slice(&VERSION.0.to_be_bytes());
bytes.extend_from_slice(&VERSION.1.to_be_bytes());
bytes.push(SCHEMA_VERSION);
bytes.extend_from_slice(&[0; PADDING_BYTES]);
bytes.extend_from_slice(&covered_next_seq.to_be_bytes());
bytes.extend_from_slice(&live.to_be_bytes());
for id in &sets.live {
bytes.extend_from_slice(&id.to_be_bytes());
}
bytes.extend_from_slice(&sweeping.to_be_bytes());
for id in &sets.sweeping {
bytes.extend_from_slice(&id.to_be_bytes());
}
let sealed = digest(DIGEST_DOMAIN, &bytes);
bytes.extend_from_slice(&sealed);
Ok(bytes)
}
pub(super) fn decode(input: &[u8]) -> Result<Self, RegistrySnapshotError> {
if input.len() < EMPTY_BYTES {
return Err(length(width(EMPTY_BYTES), input));
}
if input.get(..MAGIC.len()) != Some(MAGIC.as_slice()) {
return Err(RegistrySnapshotError::Magic);
}
let major = u16::from_be_bytes(take(input, 4)?);
let minor = u16::from_be_bytes(take(input, 6)?);
let (row_schema, domain) = match (major, minor) {
VERSION => (SCHEMA_VERSION, DIGEST_DOMAIN),
VERSION_V1 => (SCHEMA_VERSION_V1, DIGEST_DOMAIN_V1),
_ => return Err(RegistrySnapshotError::Version { major, minor }),
};
let [schema] = take::<1>(input, ROW_SCHEMA_AT)?;
if schema != row_schema {
return Err(RegistrySnapshotError::RowSchema {
expected: row_schema,
actual: schema,
});
}
if take::<PADDING_BYTES>(input, PADDING_AT)? != [0; PADDING_BYTES] {
return Err(RegistrySnapshotError::Padding);
}
let covered_next_seq = u64::from_be_bytes(take(input, COVERED_AT)?);
let live_count = u64::from_be_bytes(take(input, COUNT_AT)?);
let live_end = run_end(HEADER_BYTES, live_count);
let (sweeping_count, expected) = if (major, minor) == VERSION_V1 {
(
0,
live_end.and_then(|end| end.checked_add(width(DIGEST_BYTES))),
)
} else {
let count = live_end
.and_then(|end| usize::try_from(end).ok())
.and_then(|end| take::<SWEEPING_COUNT_BYTES>(input, end).ok())
.map(u64::from_be_bytes);
let Some(count) = count else {
return Err(length(
live_end.map_or(u64::MAX, |end| {
end.saturating_add(width(SWEEPING_COUNT_BYTES))
}),
input,
));
};
let end = live_end
.and_then(|end| end.checked_add(width(SWEEPING_COUNT_BYTES)))
.and_then(|start| run_end_at(start, count));
(
count,
end.and_then(|end| end.checked_add(width(DIGEST_BYTES))),
)
};
let expected = expected.unwrap_or(u64::MAX);
if width(input.len()) != expected {
return Err(length(expected, input));
}
let (body, stored) = input
.split_at_checked(input.len().saturating_sub(DIGEST_BYTES))
.ok_or_else(|| length(expected, input))?;
let live_bytes =
usize::try_from(live_count.saturating_mul(width(ID_BYTES))).unwrap_or(usize::MAX);
let live_run = body
.get(HEADER_BYTES..HEADER_BYTES.saturating_add(live_bytes))
.unwrap_or_default();
let live = ascending_ids(live_run)?;
let sweeping = if sweeping_count == 0 {
BTreeSet::new()
} else {
let start = HEADER_BYTES
.saturating_add(live_bytes)
.saturating_add(SWEEPING_COUNT_BYTES);
ascending_ids(body.get(start..).unwrap_or_default())?
};
if let Some(id) = live.intersection(&sweeping).next() {
return Err(RegistrySnapshotError::BothSets { id: *id });
}
if digest(domain, body).as_slice() != stored {
return Err(RegistrySnapshotError::Digest);
}
Ok(Self {
covered_next_seq,
sets: RegistrySets { live, sweeping },
})
}
}
fn run_end(start: usize, count: u64) -> Option<u64> {
run_end_at(width(start), count)
}
fn run_end_at(start: u64, count: u64) -> Option<u64> {
count
.checked_mul(width(ID_BYTES))
.and_then(|bytes| bytes.checked_add(start))
}
fn digest(domain: &[u8], body: &[u8]) -> [u8; DIGEST_BYTES] {
let mut hash = blake3::Hasher::new();
hash.update(domain);
hash.update(body);
*hash.finalize().as_bytes()
}
fn ascending_ids(run: &[u8]) -> Result<BTreeSet<u64>, RegistrySnapshotError> {
let mut ids = BTreeSet::new();
let mut previous: Option<u64> = None;
for (index, chunk) in run.chunks_exact(ID_BYTES).enumerate() {
let word: [u8; ID_BYTES] = chunk
.try_into()
.map_err(|_| RegistrySnapshotError::Length {
expected: width(ID_BYTES),
actual: width(chunk.len()),
})?;
let id = u64::from_be_bytes(word);
if let Some(previous) = previous.filter(|previous| id <= *previous) {
return Err(RegistrySnapshotError::Order {
index,
previous,
id,
});
}
previous = Some(id);
ids.insert(id);
}
Ok(ids)
}
fn length(expected: u64, input: &[u8]) -> RegistrySnapshotError {
RegistrySnapshotError::Length {
expected,
actual: width(input.len()),
}
}
fn width(bytes: usize) -> u64 {
u64::try_from(bytes).unwrap_or(u64::MAX)
}
fn take<const N: usize>(input: &[u8], start: usize) -> Result<[u8; N], RegistrySnapshotError> {
start
.checked_add(N)
.and_then(|end| input.get(start..end))
.and_then(|bytes| <[u8; N]>::try_from(bytes).ok())
.ok_or_else(|| length(width(EMPTY_BYTES), input))
}
#[derive(Clone, Copy, Debug)]
struct SnapshotAt;
impl fmt::Display for SnapshotAt {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"route {REGISTRY_STREAM_KEY} key {SNAPSHOT_KEY_TEXT}"
)
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub(super) enum RegistrySnapshotError {
#[error("registry snapshot at {at}: length differs: expected {expected}, found {actual}", at = SnapshotAt)]
Length {
expected: u64,
actual: u64,
},
#[error("registry snapshot at {at}: magic differs", at = SnapshotAt)]
Magic,
#[error("registry snapshot at {at}: codec {major}.{minor} is unsupported", at = SnapshotAt)]
Version {
major: u16,
minor: u16,
},
#[error("registry snapshot at {at}: row schema {actual} differs from {expected}", at = SnapshotAt)]
RowSchema {
expected: u8,
actual: u8,
},
#[error("registry snapshot at {at}: padding is not zero", at = SnapshotAt)]
Padding,
#[error(
"registry snapshot at {at}: id {id} at index {index} is not above {previous}",
at = SnapshotAt
)]
Order {
index: usize,
previous: u64,
id: u64,
},
#[error("registry snapshot at {at}: id {id} is both live and sweeping", at = SnapshotAt)]
BothSets {
id: u64,
},
#[error("registry snapshot at {at}: digest differs", at = SnapshotAt)]
Digest,
#[error(
"registry snapshot at {at}: covers {covered_next_seq} rows past the registry head {head}",
at = SnapshotAt
)]
AheadOfStream {
covered_next_seq: u64,
head: u64,
},
#[error(
"registry snapshot at {at}: tail from {covered_next_seq} to head {head} reaches the 256-row bound",
at = SnapshotAt
)]
TailBeyondBound {
covered_next_seq: u64,
head: u64,
},
#[error(
"registry snapshot at {at}: rows end at {tail_end}, not at head {expected_head}",
at = SnapshotAt
)]
TailMismatch {
expected_head: u64,
tail_end: u64,
},
#[error(
"registry snapshot at {at}: routed head {routed} stands below the native head {native}",
at = SnapshotAt
)]
RoutedHeadBehindNative {
routed: u64,
native: u64,
},
#[error(
"registry snapshot at {at}: the landed crossing to {head} holds a snapshot covering {covered_next_seq:?}",
at = SnapshotAt
)]
CrossingUnmatched {
head: u64,
covered_next_seq: Option<u64>,
},
#[error("registry snapshot at {at}: {count} ids exceed the frame", at = SnapshotAt)]
TooLarge {
count: usize,
},
}