1use crate::{
2 Admission, JournalBackend, JournalEntry, JournalError, JournalHead, Lease, StoredState,
3};
4use sim_kernel::{ContentId, Symbol};
5use sim_storage_port::{HostDirErrorKind, HostDirPort, NeverCancel};
6use std::{collections::BTreeMap, sync::Arc};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub struct BackendCapabilities {
11 pub linearizable_cas: bool,
13 pub durable_publish: bool,
15}
16
17#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum Failpoint {
20 BeforeObjectPublish,
21 AfterObjectPublish,
22 AfterDurabilityReceipt,
23 BeforeCas,
24 AfterCas,
25 BeforeAcknowledgement,
26}
27
28impl Failpoint {
29 fn label(self) -> &'static str {
30 match self {
31 Self::BeforeObjectPublish => "before-object-publish",
32 Self::AfterObjectPublish => "after-object-publish",
33 Self::AfterDurabilityReceipt => "after-durability-receipt",
34 Self::BeforeCas => "before-cas",
35 Self::AfterCas => "after-cas",
36 Self::BeforeAcknowledgement => "before-acknowledgement",
37 }
38 }
39}
40
41type FailHook = Arc<dyn Fn(Failpoint) -> bool + Send + Sync>;
42
43pub struct HostDirJournalBackend {
48 port: Arc<dyn HostDirPort>,
49 capabilities: BackendCapabilities,
50 work_bound: usize,
51 fail: Option<FailHook>,
52}
53
54impl HostDirJournalBackend {
55 pub fn open(
57 port: Arc<dyn HostDirPort>,
58 capabilities: BackendCapabilities,
59 work_bound: usize,
60 ) -> Result<Self, JournalError> {
61 if work_bound == 0 {
62 return Err(JournalError::WorkBoundExceeded);
63 }
64 let backend = Self {
65 port,
66 capabilities,
67 work_bound,
68 fail: None,
69 };
70 backend.read_state()?; Ok(backend)
72 }
73
74 pub fn with_failpoint_hook(
76 mut self,
77 hook: impl Fn(Failpoint) -> bool + Send + Sync + 'static,
78 ) -> Self {
79 self.fail = Some(Arc::new(hook));
80 self
81 }
82
83 pub fn capabilities(&self) -> BackendCapabilities {
85 self.capabilities
86 }
87
88 fn trip(&self, point: Failpoint) -> Result<(), JournalError> {
89 if self.fail.as_ref().is_some_and(|hook| hook(point)) {
90 Err(JournalError::InjectedCrash(point.label()))
91 } else {
92 Ok(())
93 }
94 }
95
96 fn write_capable(&self) -> Result<(), JournalError> {
97 if !self.capabilities.linearizable_cas {
98 return Err(JournalError::WriteRefused(
99 "linearizable table/cas unavailable",
100 ));
101 }
102 if !self.capabilities.durable_publish {
103 return Err(JournalError::WriteRefused(
104 "storage durability receipt unavailable",
105 ));
106 }
107 Ok(())
108 }
109
110 fn ensure_layout(&self) -> Result<(), JournalError> {
111 for path in [["objects"], ["entries"], ["temporary"]] {
112 self.port
113 .create_dir(&path.map(str::to_owned))
114 .map_err(port_error)?;
115 }
116 Ok(())
117 }
118
119 fn state_bytes(&self) -> Result<Option<Vec<u8>>, JournalError> {
120 match self.port.read(&["state".into()]) {
121 Ok(bytes) => Ok(Some(bytes)),
122 Err(e) if e.kind == HostDirErrorKind::NotFound => Ok(None),
123 Err(e) => Err(port_error(e)),
124 }
125 }
126
127 fn put_immutable(&self, path: &[String], bytes: &[u8]) -> Result<(), JournalError> {
128 let outcome = self
129 .port
130 .compare_exchange(path, None, Some(bytes), &NeverCancel)
131 .map_err(port_error)?;
132 if outcome.exchanged {
133 return Ok(());
134 }
135 if outcome.observed.as_deref() == Some(bytes) {
136 Ok(())
137 } else {
138 Err(JournalError::ConflictingObject)
139 }
140 }
141
142 fn load(&self, path: &[String]) -> Result<Vec<u8>, JournalError> {
143 self.port.read(path).map_err(port_error)
144 }
145}
146
147impl JournalBackend for HostDirJournalBackend {
148 fn acquire_lease(&self) -> Result<Lease, JournalError> {
149 self.write_capable()?;
150 self.ensure_layout()?;
151 loop {
152 let observed = self.state_bytes()?;
153 let (fence, head) = observed
154 .as_deref()
155 .map(decode_state)
156 .transpose()?
157 .unwrap_or((0, None));
158 let next = fence
159 .checked_add(1)
160 .ok_or_else(|| JournalError::Backend("fence exhausted".into()))?;
161 let replacement = encode_state(next, head.as_ref());
162 let result = self
163 .port
164 .compare_exchange(
165 &["state".into()],
166 observed.as_deref(),
167 Some(&replacement),
168 &NeverCancel,
169 )
170 .map_err(port_error)?;
171 if result.exchanged {
172 return Ok(Lease { fence: next });
173 }
174 }
175 }
176
177 fn read_state(&self) -> Result<StoredState, JournalError> {
178 let (_, head) = self
179 .state_bytes()?
180 .as_deref()
181 .map(decode_state)
182 .transpose()?
183 .unwrap_or((0, None));
184 let Some(head) = head else {
185 return Ok(StoredState::default());
186 };
187 let needed = (head.sequence as usize)
188 .checked_add(1)
189 .ok_or(JournalError::WorkBoundExceeded)?;
190 if needed > self.work_bound {
191 return Err(JournalError::WorkBoundExceeded);
192 }
193 let mut entries = BTreeMap::new();
194 let mut objects = BTreeMap::new();
195 for sequence in 0..=head.sequence {
196 let bytes = self.load(&entry_path(sequence))?;
197 let entry = decode_entry(&bytes)?;
198 if entry.sequence != sequence {
199 return Err(JournalError::CorruptState("entry location"));
200 }
201 for id in &entry.payloads {
202 if !objects.contains_key(id) {
203 if entries.len() + objects.len() >= self.work_bound {
204 return Err(JournalError::WorkBoundExceeded);
205 }
206 objects.insert(id.clone(), self.load(&object_path(id))?);
207 }
208 }
209 entries.insert(sequence, entry);
210 }
211 let state = StoredState {
212 objects,
213 entries,
214 head: Some(head),
215 };
216 crate::verify::verify_state(&state)?;
217 Ok(state)
218 }
219
220 fn admit(&self, admission: Admission) -> Result<JournalHead, JournalError> {
221 self.write_capable()?;
222 self.ensure_layout()?;
223 let observed = self.state_bytes()?;
224 let (fence, head) = observed
225 .as_deref()
226 .map(decode_state)
227 .transpose()?
228 .unwrap_or((0, None));
229 if fence != admission.fence {
230 return Err(JournalError::StaleLease);
231 }
232 if head != admission.expected {
233 return Err(JournalError::WrongHead);
234 }
235 self.trip(Failpoint::BeforeObjectPublish)?;
236 for object in &admission.objects {
237 object.verify()?;
238 self.put_immutable(&object_path(&object.id), &object.bytes)?;
239 }
240 self.trip(Failpoint::AfterObjectPublish)?;
241 for entry in &admission.entries {
242 self.put_immutable(&entry_path(entry.sequence), &encode_entry(entry))?;
243 }
244 self.trip(Failpoint::AfterDurabilityReceipt)?;
245 let last = admission.entries.last().ok_or(JournalError::EmptyBatch)?;
246 let new_head = JournalHead {
247 sequence: last.sequence,
248 entry: last.id.clone(),
249 };
250 self.trip(Failpoint::BeforeCas)?;
251 let replacement = encode_state(fence, Some(&new_head));
252 let result = self
253 .port
254 .compare_exchange(
255 &["state".into()],
256 observed.as_deref(),
257 Some(&replacement),
258 &NeverCancel,
259 )
260 .map_err(port_error)?;
261 if !result.exchanged {
262 return Err(JournalError::WrongHead);
263 }
264 self.trip(Failpoint::AfterCas)?;
265 self.trip(Failpoint::BeforeAcknowledgement)?;
266 Ok(new_head)
267 }
268}
269
270fn port_error(error: sim_storage_port::HostDirError) -> JournalError {
271 JournalError::Backend(error.to_string())
272}
273fn entry_path(sequence: u64) -> Vec<String> {
274 vec!["entries".into(), format!("{sequence:016x}")]
275}
276fn object_path(id: &ContentId) -> Vec<String> {
277 vec!["objects".into(), hex(&id.bytes)]
278}
279fn hex(bytes: &[u8]) -> String {
280 bytes.iter().map(|b| format!("{b:02x}")).collect()
281}
282
283fn encode_state(fence: u64, head: Option<&JournalHead>) -> Vec<u8> {
284 let mut out = b"SIMJSTATE1".to_vec();
285 out.extend(fence.to_be_bytes());
286 match head {
287 Some(h) => {
288 out.push(1);
289 out.extend(h.sequence.to_be_bytes());
290 put_id(&mut out, &h.entry);
291 }
292 None => out.push(0),
293 }
294 out
295}
296fn decode_state(bytes: &[u8]) -> Result<(u64, Option<JournalHead>), JournalError> {
297 let mut c = Cursor::new(bytes);
298 if c.take(10)? != b"SIMJSTATE1" {
299 return Err(JournalError::CorruptState("state format"));
300 }
301 let fence = c.u64()?;
302 let head = match c.byte()? {
303 0 => None,
304 1 => Some(JournalHead {
305 sequence: c.u64()?,
306 entry: c.id()?,
307 }),
308 _ => return Err(JournalError::CorruptState("state tag")),
309 };
310 c.end()?;
311 Ok((fence, head))
312}
313fn encode_entry(entry: &JournalEntry) -> Vec<u8> {
314 let mut out = b"SIMJENTRY1".to_vec();
315 put_id(&mut out, &entry.id);
316 out.extend(entry.sequence.to_be_bytes());
317 match &entry.previous {
318 Some(id) => {
319 out.push(1);
320 put_id(&mut out, id)
321 }
322 None => out.push(0),
323 };
324 put_text(&mut out, &entry.kind.as_qualified_str());
325 out.extend((entry.payloads.len() as u32).to_be_bytes());
326 for id in &entry.payloads {
327 put_id(&mut out, id);
328 }
329 out
330}
331fn decode_entry(bytes: &[u8]) -> Result<JournalEntry, JournalError> {
332 let mut c = Cursor::new(bytes);
333 if c.take(10)? != b"SIMJENTRY1" {
334 return Err(JournalError::CorruptState("entry format"));
335 }
336 let id = c.id()?;
337 let sequence = c.u64()?;
338 let previous = match c.byte()? {
339 0 => None,
340 1 => Some(c.id()?),
341 _ => return Err(JournalError::CorruptState("entry tag")),
342 };
343 let kind = parse_symbol(&c.text()?)?;
344 let count = c.u32()? as usize;
345 let mut payloads = Vec::with_capacity(count);
346 for _ in 0..count {
347 payloads.push(c.id()?);
348 }
349 c.end()?;
350 Ok(JournalEntry {
351 id,
352 sequence,
353 previous,
354 kind,
355 payloads,
356 })
357}
358fn put_id(out: &mut Vec<u8>, id: &ContentId) {
359 put_text(out, &id.algorithm.as_qualified_str());
360 out.extend(id.bytes)
361}
362fn put_text(out: &mut Vec<u8>, text: &str) {
363 out.extend((text.len() as u32).to_be_bytes());
364 out.extend(text.as_bytes())
365}
366fn parse_symbol(text: &str) -> Result<Symbol, JournalError> {
367 match text.split_once('/') {
368 Some((n, v)) if !n.is_empty() && !v.is_empty() => Ok(Symbol::qualified(n, v)),
369 None => Symbol::checked(text).map_err(|_| JournalError::CorruptState("symbol")),
370 _ => Err(JournalError::CorruptState("symbol")),
371 }
372}
373struct Cursor<'a> {
374 bytes: &'a [u8],
375 at: usize,
376}
377impl<'a> Cursor<'a> {
378 fn new(bytes: &'a [u8]) -> Self {
379 Self { bytes, at: 0 }
380 }
381 fn take(&mut self, n: usize) -> Result<&'a [u8], JournalError> {
382 let end = self
383 .at
384 .checked_add(n)
385 .ok_or(JournalError::CorruptState("length"))?;
386 let value = self
387 .bytes
388 .get(self.at..end)
389 .ok_or(JournalError::CorruptState("truncated"))?;
390 self.at = end;
391 Ok(value)
392 }
393 fn byte(&mut self) -> Result<u8, JournalError> {
394 Ok(self.take(1)?[0])
395 }
396 fn u32(&mut self) -> Result<u32, JournalError> {
397 Ok(u32::from_be_bytes(self.take(4)?.try_into().unwrap()))
398 }
399 fn u64(&mut self) -> Result<u64, JournalError> {
400 Ok(u64::from_be_bytes(self.take(8)?.try_into().unwrap()))
401 }
402 fn text(&mut self) -> Result<String, JournalError> {
403 let n = self.u32()? as usize;
404 String::from_utf8(self.take(n)?.to_vec()).map_err(|_| JournalError::CorruptState("utf8"))
405 }
406 fn id(&mut self) -> Result<ContentId, JournalError> {
407 let algorithm = parse_symbol(&self.text()?)?;
408 let bytes = self.take(32)?.try_into().unwrap();
409 Ok(ContentId::from_bytes(algorithm, bytes))
410 }
411 fn end(&self) -> Result<(), JournalError> {
412 if self.at == self.bytes.len() {
413 Ok(())
414 } else {
415 Err(JournalError::CorruptState("trailing bytes"))
416 }
417 }
418}