use crate::sync::ballot::Ballot;
use crate::sync::topology::SyncNodeId;
use super::buffer::WalError;
pub(crate) const TAG_PROMISE: u8 = 0x04;
const U32_SIZE: usize = 4;
const U64_SIZE: usize = 8;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PromiseRecord {
pub promised: Ballot,
pub owner_epoch: Option<Ballot>,
pub persisted_max_minted: u64,
}
impl PromiseRecord {
#[must_use]
pub fn initial() -> Self {
Self {
promised: Ballot::bottom(),
owner_epoch: None,
persisted_max_minted: 0,
}
}
#[must_use]
pub fn serialise(&self) -> Vec<u8> {
let mut payload = Vec::new();
payload.push(TAG_PROMISE);
append_ballot(&mut payload, &self.promised);
match &self.owner_epoch {
Some(owner) => {
payload.push(1);
append_ballot(&mut payload, owner);
}
None => payload.push(0),
}
payload.extend_from_slice(&self.persisted_max_minted.to_le_bytes());
let crc = crc32fast::hash(&payload);
payload.extend_from_slice(&crc.to_le_bytes());
payload
}
pub(crate) fn decode(payload: &[u8]) -> Result<Self, WalError> {
let mut cursor = Cursor::new(payload);
let tag = cursor.read_u8()?;
if tag != TAG_PROMISE {
return Err(WalError::InvalidTag { found: tag });
}
let promised = read_ballot(&mut cursor)?;
let owner_epoch = match cursor.read_u8()? {
0 => None,
1 => Some(read_ballot(&mut cursor)?),
found => return Err(WalError::InvalidTag { found }),
};
let persisted_max_minted = cursor.read_u64()?;
cursor.finish()?;
Ok(Self {
promised,
owner_epoch,
persisted_max_minted,
})
}
}
pub(crate) fn is_promise_payload(payload: &[u8]) -> bool {
payload.first() == Some(&TAG_PROMISE)
}
fn append_ballot(bytes: &mut Vec<u8>, ballot: &Ballot) {
bytes.extend_from_slice(&ballot.counter.to_le_bytes());
let node = ballot.node.as_str().as_bytes();
bytes.extend_from_slice(&(node.len() as u32).to_le_bytes());
bytes.extend_from_slice(node);
}
fn read_ballot(cursor: &mut Cursor<'_>) -> Result<Ballot, WalError> {
let counter = cursor.read_u64()?;
let node_bytes = cursor.read_len_prefixed()?;
let node = std::str::from_utf8(node_bytes)
.map_err(|_| WalError::TreeError("promise node id is not valid utf-8".to_owned()))?;
Ok(Ballot::new(counter, SyncNodeId::new(node)))
}
struct Cursor<'a> {
bytes: &'a [u8],
offset: usize,
}
impl<'a> Cursor<'a> {
const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, offset: 0 }
}
fn read_u8(&mut self) -> Result<u8, WalError> {
let byte = *self.bytes.get(self.offset).ok_or(WalError::Truncated)?;
self.offset += 1;
Ok(byte)
}
fn read_bytes(&mut self, len: usize) -> Result<&'a [u8], WalError> {
let end = self
.offset
.checked_add(len)
.ok_or(WalError::LengthOverflow)?;
let slice = self
.bytes
.get(self.offset..end)
.ok_or(WalError::Truncated)?;
self.offset = end;
Ok(slice)
}
fn read_u32(&mut self) -> Result<u32, WalError> {
let bytes = self.read_bytes(U32_SIZE)?;
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
fn read_u64(&mut self) -> Result<u64, WalError> {
let bytes = self.read_bytes(U64_SIZE)?;
let mut raw = [0u8; U64_SIZE];
raw.copy_from_slice(bytes);
Ok(u64::from_le_bytes(raw))
}
fn read_len_prefixed(&mut self) -> Result<&'a [u8], WalError> {
let len = usize::try_from(self.read_u32()?).map_err(|_| WalError::LengthOverflow)?;
self.read_bytes(len)
}
const fn finish(&self) -> Result<(), WalError> {
let trailing = self.bytes.len().saturating_sub(self.offset);
if trailing == 0 {
Ok(())
} else {
Err(WalError::TrailingBytes { trailing })
}
}
}
#[cfg(test)]
mod tests {
use super::{PromiseRecord, is_promise_payload};
use crate::sync::ballot::Ballot;
use crate::sync::topology::SyncNodeId;
fn ballot(counter: u64, node: &str) -> Ballot {
Ballot::new(counter, SyncNodeId::from(node))
}
fn payload_of(record: &PromiseRecord) -> Vec<u8> {
let mut bytes = record.serialise();
bytes.truncate(bytes.len() - 4);
bytes
}
#[test]
fn round_trips_full_state() -> Result<(), super::WalError> {
let record = PromiseRecord {
promised: ballot(5, "node-x"),
owner_epoch: Some(ballot(5, "node-x")),
persisted_max_minted: 9,
};
assert_eq!(PromiseRecord::decode(&payload_of(&record))?, record);
Ok(())
}
#[test]
fn round_trips_without_owner_epoch() -> Result<(), super::WalError> {
let record = PromiseRecord {
promised: ballot(3, "node-y"),
owner_epoch: None,
persisted_max_minted: 3,
};
assert_eq!(PromiseRecord::decode(&payload_of(&record))?, record);
Ok(())
}
#[test]
fn initial_is_bottom_none_zero() {
let initial = PromiseRecord::initial();
assert_eq!(initial.promised, Ballot::bottom());
assert_eq!(initial.owner_epoch, None);
assert_eq!(initial.persisted_max_minted, 0);
}
#[test]
fn promise_payload_is_distinguishable() {
assert!(is_promise_payload(&payload_of(&PromiseRecord::initial())));
assert!(!is_promise_payload(&[0x01, 0, 0]));
}
#[test]
fn decode_rejects_trailing_bytes() {
let mut payload = payload_of(&PromiseRecord::initial());
payload.push(0xaa);
assert!(matches!(
PromiseRecord::decode(&payload),
Err(super::WalError::TrailingBytes { .. })
));
}
}