1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct JournalGeneration(pub u128);
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
16pub struct OpNumber(pub u64);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct WriteTicket {
21 generation: JournalGeneration,
22 first: OpNumber,
23 through: OpNumber,
24}
25
26impl WriteTicket {
27 pub const fn generation(self) -> JournalGeneration {
29 self.generation
30 }
31 pub const fn first(self) -> OpNumber {
33 self.first
34 }
35 pub const fn through(self) -> OpNumber {
37 self.through
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct SyncTicket {
44 generation: JournalGeneration,
45 through: OpNumber,
46}
47
48impl SyncTicket {
49 pub const fn generation(self) -> JournalGeneration {
51 self.generation
52 }
53 pub const fn through(self) -> OpNumber {
55 self.through
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct JournalSnapshot {
62 pub generation: JournalGeneration,
64 pub accepted: OpNumber,
66 pub written: OpNumber,
68 pub durable: OpNumber,
70 pub faulted: bool,
72}
73
74#[derive(Debug)]
76pub struct JournalProgress {
77 state: JournalSnapshot,
78}
79
80impl JournalProgress {
81 pub const fn recover(generation: JournalGeneration, through: OpNumber) -> Self {
87 Self {
88 state: JournalSnapshot {
89 generation,
90 accepted: through,
91 written: through,
92 durable: through,
93 faulted: false,
94 },
95 }
96 }
97
98 pub const fn snapshot(&self) -> JournalSnapshot {
100 self.state
101 }
102
103 pub fn admit(&mut self, operations: u64) -> Result<WriteTicket, ProgressError> {
106 self.require_active(self.state.generation)?;
107 if operations == 0 {
108 return Err(ProgressError::EmptyGroup);
109 }
110 let through = self
111 .state
112 .accepted
113 .0
114 .checked_add(operations)
115 .ok_or(ProgressError::OpExhausted)?;
116 let ticket = WriteTicket {
117 generation: self.state.generation,
118 first: OpNumber(self.state.accepted.0 + 1),
119 through: OpNumber(through),
120 };
121 self.state.accepted = ticket.through;
122 Ok(ticket)
123 }
124
125 pub fn complete_write(&mut self, ticket: WriteTicket) -> Result<(), ProgressError> {
128 self.require_active(ticket.generation)?;
129 if ticket.through > self.state.accepted {
130 return Err(ProgressError::BeyondAccepted);
131 }
132 if ticket.through <= self.state.written {
133 return Ok(());
134 }
135 if Some(ticket.first.0) != self.state.written.0.checked_add(1) {
136 return Err(ProgressError::WriteGap);
137 }
138 self.state.written = ticket.through;
139 Ok(())
140 }
141
142 pub fn begin_sync(&self) -> Result<SyncTicket, ProgressError> {
144 self.require_active(self.state.generation)?;
145 Ok(SyncTicket {
146 generation: self.state.generation,
147 through: self.state.written,
148 })
149 }
150
151 pub fn complete_sync(&mut self, ticket: SyncTicket) -> Result<(), ProgressError> {
154 self.require_active(ticket.generation)?;
155 if ticket.through > self.state.written {
156 return Err(ProgressError::BeyondWritten);
157 }
158 self.state.durable = self.state.durable.max(ticket.through);
159 Ok(())
160 }
161
162 pub fn complete_durable_write(&mut self, ticket: WriteTicket) -> Result<(), ProgressError> {
166 self.complete_write(ticket)?;
167 self.state.durable = self.state.durable.max(ticket.through);
168 Ok(())
169 }
170
171 pub fn fail(&mut self, generation: JournalGeneration) -> Result<(), ProgressError> {
174 if generation != self.state.generation {
175 return Err(ProgressError::StaleGeneration);
176 }
177 self.state.faulted = true;
178 Ok(())
179 }
180
181 fn require_active(&self, generation: JournalGeneration) -> Result<(), ProgressError> {
182 if generation != self.state.generation {
183 return Err(ProgressError::StaleGeneration);
184 }
185 if self.state.faulted {
186 return Err(ProgressError::Faulted);
187 }
188 Ok(())
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
194pub enum ProgressError {
195 #[error("journal generation is faulted")]
197 Faulted,
198 #[error("stale journal generation")]
200 StaleGeneration,
201 #[error("empty journal group")]
203 EmptyGroup,
204 #[error("journal operation number exhausted")]
206 OpExhausted,
207 #[error("write completion exceeds admitted prefix")]
209 BeyondAccepted,
210 #[error("write completion leaves a journal gap")]
212 WriteGap,
213 #[error("sync completion exceeds written prefix")]
215 BeyondWritten,
216}
217
218#[cfg(kani)]
219mod proofs {
220 use super::*;
221
222 #[kani::proof]
223 fn transitions_preserve_prefix_order_or_leave_state_unchanged() {
224 let before = JournalSnapshot {
225 generation: JournalGeneration(kani::any()),
226 accepted: OpNumber(kani::any()),
227 written: OpNumber(kani::any()),
228 durable: OpNumber(kani::any()),
229 faulted: kani::any(),
230 };
231 kani::assume(before.durable <= before.written && before.written <= before.accepted);
234 let mut progress = JournalProgress { state: before };
235 let generation = JournalGeneration(kani::any());
236 let through = OpNumber(kani::any());
237 let result = match kani::any::<u8>() {
238 0 => progress.admit(kani::any()).map(|_| ()),
239 1 => progress.complete_write(WriteTicket {
240 generation,
241 first: OpNumber(kani::any()),
242 through,
243 }),
244 2 => progress.complete_sync(SyncTicket {
245 generation,
246 through,
247 }),
248 3 => progress.complete_durable_write(WriteTicket {
249 generation,
250 first: OpNumber(kani::any()),
251 through,
252 }),
253 _ => progress.fail(generation),
254 };
255 let after = progress.snapshot();
256 assert!(after.durable <= after.written && after.written <= after.accepted);
257 assert!(after.durable >= before.durable);
258 assert!(after.written >= before.written);
259 assert!(after.accepted >= before.accepted);
260 if result.is_err() {
261 assert_eq!(after, before);
262 }
263 }
264
265 #[kani::proof]
266 fn admission_is_atomic_and_cannot_wrap() {
267 let start: u64 = kani::any();
268 let count: u64 = kani::any();
269 let mut progress = JournalProgress::recover(JournalGeneration(1), OpNumber(start));
270 let before = progress.snapshot();
271 match progress.admit(count) {
272 Ok(ticket) => {
273 assert!(count > 0);
274 assert!(ticket.first.0 > start);
275 assert_eq!(Some(ticket.through.0), start.checked_add(count));
276 assert_eq!(progress.snapshot().written, before.written);
277 assert_eq!(progress.snapshot().durable, before.durable);
278 }
279 Err(_) => {
280 assert!(count == 0 || start.checked_add(count).is_none());
281 assert_eq!(progress.snapshot(), before);
282 }
283 }
284 }
285
286 #[kani::proof]
287 fn sync_cannot_acknowledge_later_writes() {
288 let start: u64 = kani::any();
289 let first_count = u64::from(kani::any::<u8>()) + 1;
290 let second_count = u64::from(kani::any::<u8>()) + 1;
291 let mut progress = JournalProgress::recover(JournalGeneration(1), OpNumber(start));
292 let Ok(first) = progress.admit(first_count) else {
293 return;
294 };
295 progress.complete_write(first).unwrap();
296 let sync = progress.begin_sync().unwrap();
297 let Ok(second) = progress.admit(second_count) else {
298 return;
299 };
300 progress.complete_write(second).unwrap();
301 progress.complete_sync(sync).unwrap();
302 assert_eq!(progress.snapshot().durable, first.through);
303 assert!(progress.snapshot().durable < progress.snapshot().written);
304 }
305}