#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JournalGeneration(pub u128);
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct OpNumber(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriteTicket {
generation: JournalGeneration,
first: OpNumber,
through: OpNumber,
}
impl WriteTicket {
pub const fn generation(self) -> JournalGeneration {
self.generation
}
pub const fn first(self) -> OpNumber {
self.first
}
pub const fn through(self) -> OpNumber {
self.through
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyncTicket {
generation: JournalGeneration,
through: OpNumber,
}
impl SyncTicket {
pub const fn generation(self) -> JournalGeneration {
self.generation
}
pub const fn through(self) -> OpNumber {
self.through
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct JournalSnapshot {
pub generation: JournalGeneration,
pub accepted: OpNumber,
pub written: OpNumber,
pub durable: OpNumber,
pub faulted: bool,
}
#[derive(Debug)]
pub struct JournalProgress {
state: JournalSnapshot,
}
impl JournalProgress {
pub const fn recover(generation: JournalGeneration, through: OpNumber) -> Self {
Self {
state: JournalSnapshot {
generation,
accepted: through,
written: through,
durable: through,
faulted: false,
},
}
}
pub const fn snapshot(&self) -> JournalSnapshot {
self.state
}
pub fn admit(&mut self, operations: u64) -> Result<WriteTicket, ProgressError> {
self.require_active(self.state.generation)?;
if operations == 0 {
return Err(ProgressError::EmptyGroup);
}
let through = self
.state
.accepted
.0
.checked_add(operations)
.ok_or(ProgressError::OpExhausted)?;
let ticket = WriteTicket {
generation: self.state.generation,
first: OpNumber(self.state.accepted.0 + 1),
through: OpNumber(through),
};
self.state.accepted = ticket.through;
Ok(ticket)
}
pub fn complete_write(&mut self, ticket: WriteTicket) -> Result<(), ProgressError> {
self.require_active(ticket.generation)?;
if ticket.through > self.state.accepted {
return Err(ProgressError::BeyondAccepted);
}
if ticket.through <= self.state.written {
return Ok(());
}
if Some(ticket.first.0) != self.state.written.0.checked_add(1) {
return Err(ProgressError::WriteGap);
}
self.state.written = ticket.through;
Ok(())
}
pub fn begin_sync(&self) -> Result<SyncTicket, ProgressError> {
self.require_active(self.state.generation)?;
Ok(SyncTicket {
generation: self.state.generation,
through: self.state.written,
})
}
pub fn complete_sync(&mut self, ticket: SyncTicket) -> Result<(), ProgressError> {
self.require_active(ticket.generation)?;
if ticket.through > self.state.written {
return Err(ProgressError::BeyondWritten);
}
self.state.durable = self.state.durable.max(ticket.through);
Ok(())
}
pub fn complete_durable_write(&mut self, ticket: WriteTicket) -> Result<(), ProgressError> {
self.complete_write(ticket)?;
self.state.durable = self.state.durable.max(ticket.through);
Ok(())
}
pub fn fail(&mut self, generation: JournalGeneration) -> Result<(), ProgressError> {
if generation != self.state.generation {
return Err(ProgressError::StaleGeneration);
}
self.state.faulted = true;
Ok(())
}
fn require_active(&self, generation: JournalGeneration) -> Result<(), ProgressError> {
if generation != self.state.generation {
return Err(ProgressError::StaleGeneration);
}
if self.state.faulted {
return Err(ProgressError::Faulted);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ProgressError {
#[error("journal generation is faulted")]
Faulted,
#[error("stale journal generation")]
StaleGeneration,
#[error("empty journal group")]
EmptyGroup,
#[error("journal operation number exhausted")]
OpExhausted,
#[error("write completion exceeds admitted prefix")]
BeyondAccepted,
#[error("write completion leaves a journal gap")]
WriteGap,
#[error("sync completion exceeds written prefix")]
BeyondWritten,
}
#[cfg(kani)]
mod proofs {
use super::*;
#[kani::proof]
fn transitions_preserve_prefix_order_or_leave_state_unchanged() {
let before = JournalSnapshot {
generation: JournalGeneration(kani::any()),
accepted: OpNumber(kani::any()),
written: OpNumber(kani::any()),
durable: OpNumber(kani::any()),
faulted: kani::any(),
};
kani::assume(before.durable <= before.written && before.written <= before.accepted);
let mut progress = JournalProgress { state: before };
let generation = JournalGeneration(kani::any());
let through = OpNumber(kani::any());
let result = match kani::any::<u8>() {
0 => progress.admit(kani::any()).map(|_| ()),
1 => progress.complete_write(WriteTicket {
generation,
first: OpNumber(kani::any()),
through,
}),
2 => progress.complete_sync(SyncTicket {
generation,
through,
}),
3 => progress.complete_durable_write(WriteTicket {
generation,
first: OpNumber(kani::any()),
through,
}),
_ => progress.fail(generation),
};
let after = progress.snapshot();
assert!(after.durable <= after.written && after.written <= after.accepted);
assert!(after.durable >= before.durable);
assert!(after.written >= before.written);
assert!(after.accepted >= before.accepted);
if result.is_err() {
assert_eq!(after, before);
}
}
#[kani::proof]
fn admission_is_atomic_and_cannot_wrap() {
let start: u64 = kani::any();
let count: u64 = kani::any();
let mut progress = JournalProgress::recover(JournalGeneration(1), OpNumber(start));
let before = progress.snapshot();
match progress.admit(count) {
Ok(ticket) => {
assert!(count > 0);
assert!(ticket.first.0 > start);
assert_eq!(Some(ticket.through.0), start.checked_add(count));
assert_eq!(progress.snapshot().written, before.written);
assert_eq!(progress.snapshot().durable, before.durable);
}
Err(_) => {
assert!(count == 0 || start.checked_add(count).is_none());
assert_eq!(progress.snapshot(), before);
}
}
}
#[kani::proof]
fn sync_cannot_acknowledge_later_writes() {
let start: u64 = kani::any();
let first_count = u64::from(kani::any::<u8>()) + 1;
let second_count = u64::from(kani::any::<u8>()) + 1;
let mut progress = JournalProgress::recover(JournalGeneration(1), OpNumber(start));
let Ok(first) = progress.admit(first_count) else {
return;
};
progress.complete_write(first).unwrap();
let sync = progress.begin_sync().unwrap();
let Ok(second) = progress.admit(second_count) else {
return;
};
progress.complete_write(second).unwrap();
progress.complete_sync(sync).unwrap();
assert_eq!(progress.snapshot().durable, first.through);
assert!(progress.snapshot().durable < progress.snapshot().written);
}
}