1use std::collections::VecDeque;
4use std::io::IoSlice;
5use std::ops::Range;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::{Condvar, Mutex, MutexGuard};
10
11use crate::format::RegistryError;
12use crate::lifecycle::durability::{DurabilityPolicy, SyncRequirement};
13use crate::manifest::io::DurableLog;
14use crate::vfs::{SyncKind, Vfs, VfsFile};
15
16pub mod header;
17pub mod record;
18pub mod replay;
19
20use header::encode_header;
21use record::{
22 RecordEncodeError, RecordError, WalRecord, append_record_into, encode_record_into,
23 encoded_record_len,
24};
25use replay::{ReplayTerminator, replay};
26
27pub const DEFAULT_MAX_GROUP_BYTES: usize = 1_048_576;
29pub const DEFAULT_MAX_GROUP_BYTES_DURABLE: usize = 16 * 1_048_576;
36
37#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
39pub struct LogSeq(u64);
40
41impl LogSeq {
42 #[must_use]
44 pub const fn new(value: u64) -> Self {
45 Self(value)
46 }
47
48 #[must_use]
50 pub const fn get(self) -> u64 {
51 self.0
52 }
53}
54
55pub const GROUP_SIZE_HISTOGRAM_BUCKETS: usize = 18;
57pub const RECENT_GROUP_LIMIT: usize = 64;
59
60#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct VisibleRecord {
63 pub seq: LogSeq,
65 pub op: u16,
67 encoded: Arc<Vec<u8>>,
68 encoded_range: Range<usize>,
69}
70
71impl VisibleRecord {
72 fn writer_owned(
73 seq: LogSeq,
74 op: u16,
75 encoded: Arc<Vec<u8>>,
76 encoded_range: Range<usize>,
77 ) -> Self {
78 Self {
79 seq,
80 op,
81 encoded,
82 encoded_range,
83 }
84 }
85
86 fn recovered(seq: LogSeq, op: u16, encoded: Arc<Vec<u8>>, encoded_range: Range<usize>) -> Self {
87 Self {
88 seq,
89 op,
90 encoded,
91 encoded_range,
92 }
93 }
94
95 pub fn payload(&self) -> Result<&[u8], VisibleRecordError> {
97 let encoded = self.encoded.get(self.encoded_range.clone()).ok_or(
98 VisibleRecordError::InvalidEncodedRange {
99 start: self.encoded_range.start,
100 end: self.encoded_range.end,
101 available: self.encoded.len(),
102 },
103 )?;
104 decode_visible(encoded).map_err(VisibleRecordError::Record)
105 }
106
107 pub(crate) fn encoded(&self) -> Result<&[u8], VisibleRecordError> {
109 self.encoded.get(self.encoded_range.clone()).ok_or(
110 VisibleRecordError::InvalidEncodedRange {
111 start: self.encoded_range.start,
112 end: self.encoded_range.end,
113 available: self.encoded.len(),
114 },
115 )
116 }
117
118 #[must_use]
120 pub fn encoded_len(&self) -> usize {
121 self.encoded_range
122 .end
123 .saturating_sub(self.encoded_range.start)
124 }
125}
126
127fn decode_visible(encoded: &[u8]) -> Result<&[u8], RecordError> {
128 record::decode_record(encoded).map(|decoded| decoded.record.payload)
129}
130
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
133pub struct CompletedGroup {
134 pub records: usize,
136 pub encoded_bytes: usize,
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
142pub struct GroupSizeHistogram {
143 pub buckets: [u64; GROUP_SIZE_HISTOGRAM_BUCKETS],
145}
146
147impl GroupSizeHistogram {
148 const fn new() -> Self {
149 Self {
150 buckets: [0; GROUP_SIZE_HISTOGRAM_BUCKETS],
151 }
152 }
153
154 fn record(&mut self, records: usize) {
155 let bucket = if records == 0 {
156 0
157 } else {
158 (usize::BITS
159 .saturating_sub(records.leading_zeros())
160 .saturating_sub(1) as usize)
161 .min(GROUP_SIZE_HISTOGRAM_BUCKETS.saturating_sub(1))
162 };
163 if let Some(count) = self.buckets.get_mut(bucket) {
164 *count = count.saturating_add(1);
165 }
166 }
167}
168
169#[derive(Clone, Debug, Eq, PartialEq)]
171pub struct WalWriterStats {
172 pub completed_groups: u64,
174 pub completed_records: u64,
176 pub completed_bytes: u64,
178 pub group_size_histogram: GroupSizeHistogram,
180 pub recent_groups: Vec<CompletedGroup>,
182 pub pending_records: usize,
184 pub visible_end: Option<LogSeq>,
186 pub durable_end: Option<LogSeq>,
188 pub retained_records: usize,
190 pub retained_bytes: usize,
192 pub encoded_buffer_allocations: u64,
194}
195
196#[derive(Clone, Debug)]
197struct Failure {
198 kind: std::io::ErrorKind,
199 detail: Arc<str>,
200}
201
202#[derive(Debug)]
203struct WriterState {
204 next_seq: u64,
205 visible: VecDeque<VisibleRecord>,
206 retained_bytes: usize,
207 durable_end: Option<LogSeq>,
208 pending_groups: VecDeque<Group>,
209 spare_chunks: Vec<EncodedChunk>,
210 barrier_in_flight: bool,
211 failure: Option<Arc<Failure>>,
212 completed_groups: u64,
213 completed_records: u64,
214 completed_bytes: u64,
215 encoded_buffer_allocations: u64,
216 group_size_histogram: GroupSizeHistogram,
217 recent_groups: VecDeque<CompletedGroup>,
218}
219
220#[derive(Debug)]
221struct EncodedChunk {
222 encoded: Arc<Vec<u8>>,
223 range: Range<usize>,
224}
225
226impl EncodedChunk {
227 fn bytes(&self) -> std::io::Result<&[u8]> {
228 self.encoded
229 .get(self.range.clone())
230 .ok_or_else(|| std::io::Error::other("invalid WAL pending chunk range"))
231 }
232}
233
234#[derive(Debug)]
235struct Group {
236 chunks: Vec<EncodedChunk>,
237 encoded_bytes: usize,
238 records: usize,
239 last_seq: Option<LogSeq>,
240}
241
242pub struct WalWriter {
248 file: Mutex<Box<dyn VfsFile>>,
249 created_directory_sync: Mutex<Option<CreatedDirectorySync>>,
250 state: Mutex<WriterState>,
251 changed: Condvar,
252 sync: SyncRequirement,
253 max_group_bytes: usize,
254 durable_progress: AtomicU64,
255 io_work: [AtomicU64; 4],
256}
257
258struct CreatedDirectorySync {
259 vfs: Arc<dyn Vfs>,
260 directory: PathBuf,
261 kind: SyncKind,
262}
263
264impl WalWriter {
265 pub fn create(
270 vfs: &dyn Vfs,
271 path: &Path,
272 first_seq: LogSeq,
273 policy: DurabilityPolicy,
274 ) -> Result<Self, WalWriteError> {
275 let max_group_bytes = default_max_group_bytes(policy);
276 Self::create_with_max_group_bytes(vfs, path, first_seq, policy, max_group_bytes)
277 }
278
279 pub(crate) fn resume(
280 vfs: &dyn Vfs,
281 path: &Path,
282 recovered: CleanWalReader,
283 policy: DurabilityPolicy,
284 ) -> Result<Self, WalWriteError> {
285 let max_group_bytes = default_max_group_bytes(policy);
286 let next_seq = recovered.next_seq;
287 let durable_end = recovered.durable_end;
288 let durable_progress = durable_end.map_or(0, LogSeq::get);
289 let visible = VecDeque::from(recovered.records);
290 let retained_bytes = visible.iter().try_fold(0_usize, |total, record| {
291 total
292 .checked_add(record.encoded_len())
293 .ok_or(WalWriteError::RecoveredBytesOverflow)
294 })?;
295 let file = vfs.open_append(path).map_err(WalWriteError::from_io)?;
296 Ok(Self {
297 file: Mutex::new(file),
298 created_directory_sync: Mutex::new(None),
299 state: Mutex::new(WriterState {
300 next_seq,
301 visible,
302 retained_bytes,
303 durable_end,
304 pending_groups: VecDeque::with_capacity(2),
305 spare_chunks: Vec::with_capacity(16),
306 barrier_in_flight: false,
307 failure: None,
308 completed_groups: 0,
309 completed_records: 0,
310 completed_bytes: 0,
311 encoded_buffer_allocations: 0,
312 group_size_histogram: GroupSizeHistogram::new(),
313 recent_groups: VecDeque::with_capacity(RECENT_GROUP_LIMIT),
314 }),
315 changed: Condvar::new(),
316 sync: policy.data_file_sync(),
317 max_group_bytes,
318 durable_progress: AtomicU64::new(durable_progress),
319 io_work: std::array::from_fn(|_| AtomicU64::new(0)),
320 })
321 }
322
323 pub fn create_with_max_group_bytes(
325 vfs: &dyn Vfs,
326 path: &Path,
327 first_seq: LogSeq,
328 policy: DurabilityPolicy,
329 max_group_bytes: usize,
330 ) -> Result<Self, WalWriteError> {
331 match vfs.open(path) {
332 Ok(0) => {}
333 Ok(length) => return Err(WalWriteError::NonEmptyWal { length }),
334 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
335 Err(error) => return Err(WalWriteError::from_io(error)),
336 }
337 let header = encode_header(first_seq).map_err(WalWriteError::Header)?;
338 if max_group_bytes < header.len() {
339 return Err(WalWriteError::GroupTooLarge {
340 encoded_bytes: header.len(),
341 max_group_bytes,
342 });
343 }
344 let file = vfs.open_append(path).map_err(WalWriteError::from_io)?;
345 let header_len = header.len();
346 let header = Arc::new(header);
347 let mut header_chunks = Vec::with_capacity(16);
348 header_chunks.push(EncodedChunk {
349 range: 0..header_len,
350 encoded: header,
351 });
352 let mut pending_groups = VecDeque::with_capacity(2);
353 pending_groups.push_back(Group {
354 chunks: header_chunks,
355 encoded_bytes: header_len,
356 records: 0,
357 last_seq: None,
358 });
359 let spare_chunks = Vec::with_capacity(16);
360 let recent_groups = VecDeque::with_capacity(RECENT_GROUP_LIMIT);
361 Ok(Self {
362 file: Mutex::new(file),
363 created_directory_sync: Mutex::new(None),
364 state: Mutex::new(WriterState {
365 next_seq: first_seq.get(),
366 visible: VecDeque::new(),
367 retained_bytes: 0,
368 durable_end: None,
369 pending_groups,
370 spare_chunks,
371 barrier_in_flight: false,
372 failure: None,
373 completed_groups: 0,
374 completed_records: 0,
375 completed_bytes: 0,
376 encoded_buffer_allocations: 0,
377 group_size_histogram: GroupSizeHistogram::new(),
378 recent_groups,
379 }),
380 changed: Condvar::new(),
381 sync: policy.data_file_sync(),
382 max_group_bytes,
383 durable_progress: AtomicU64::new(first_seq.get().saturating_sub(1)),
384 io_work: std::array::from_fn(|_| AtomicU64::new(0)),
385 })
386 }
387
388 pub(crate) fn create_store_wal(
389 vfs: Arc<dyn Vfs>,
390 directory: &Path,
391 path: &Path,
392 first_seq: LogSeq,
393 policy: DurabilityPolicy,
394 ) -> Result<Self, WalWriteError> {
395 let mut writer = Self::create(vfs.as_ref(), path, first_seq, policy)?;
396 if let SyncRequirement::Sync(kind) = policy.directory_sync() {
397 writer.created_directory_sync = Mutex::new(Some(CreatedDirectorySync {
398 vfs,
399 directory: directory.to_path_buf(),
400 kind,
401 }));
402 }
403 Ok(writer)
404 }
405
406 pub fn commit(&self, op: u16, payload: &[u8]) -> Result<LogSeq, WalWriteError> {
409 let (seq, leader) = self.stage(op, payload)?;
410 if leader {
411 self.flush_as_leader()?;
412 }
413 Ok(seq)
414 }
415
416 pub fn commit_durable(&self, op: u16, payload: &[u8]) -> Result<LogSeq, WalWriteError> {
418 let (seq, leader) = self.stage(op, payload)?;
419 if leader {
420 self.flush_as_leader()?;
421 }
422 self.wait_until_durable(seq)?;
423 Ok(seq)
424 }
425
426 pub fn commit_many(&self, records: &[(u16, &[u8])]) -> Result<Range<LogSeq>, WalWriteError> {
440 let (range, leader) = self.stage_many(records)?;
441 if leader {
442 self.flush_as_leader()?;
443 }
444 Ok(range)
445 }
446
447 pub fn commit_many_durable(
460 &self,
461 records: &[(u16, &[u8])],
462 ) -> Result<Range<LogSeq>, WalWriteError> {
463 let (range, leader) = self.stage_many(records)?;
464 if leader {
465 self.flush_as_leader()?;
466 }
467 if range.start != range.end {
468 let last = LogSeq::new(range.end.get().saturating_sub(1));
469 self.wait_until_durable(last)?;
470 }
471 Ok(range)
472 }
473
474 fn stage_many(&self, records: &[(u16, &[u8])]) -> Result<(Range<LogSeq>, bool), WalWriteError> {
475 self.stage_many_with_encoder(records, append_record_into)
476 }
477
478 fn stage_many_with_encoder<F>(
479 &self,
480 records: &[(u16, &[u8])],
481 mut encode: F,
482 ) -> Result<(Range<LogSeq>, bool), WalWriteError>
483 where
484 F: for<'a> FnMut(WalRecord<'a>, &mut Vec<u8>) -> Result<(), RecordEncodeError>,
485 {
486 if records.is_empty() {
487 let state = self.lock_state()?;
488 if let Some(failure) = &state.failure {
489 return Err(WalWriteError::failed(failure));
490 }
491 let next = LogSeq::new(state.next_seq);
492 return Ok((next..next, false));
493 }
494 let encoded_lengths = records
495 .iter()
496 .map(|&(op, payload)| {
497 let encoded_len =
498 encoded_record_len(payload.len()).map_err(WalWriteError::Record)?;
499 Ok((op, payload, encoded_len))
500 })
501 .collect::<Result<Vec<_>, WalWriteError>>()?;
502 let batch_bytes = encoded_lengths
503 .iter()
504 .fold(0_usize, |total, record| total.saturating_add(record.2));
505 if let Some((_, _, encoded_bytes)) = encoded_lengths
506 .iter()
507 .find(|(_, _, encoded_bytes)| *encoded_bytes > self.max_group_bytes)
508 {
509 return Err(WalWriteError::GroupTooLarge {
510 encoded_bytes: *encoded_bytes,
511 max_group_bytes: self.max_group_bytes,
512 });
513 }
514 let mut encoded = Arc::new(Vec::with_capacity(batch_bytes));
515 let mut state = self.lock_state()?;
516 if let Some(failure) = &state.failure {
517 return Err(WalWriteError::failed(failure));
518 }
519 Self::ensure_pending_group(&mut state);
520 let first_encoded_len = encoded_lengths
521 .first()
522 .map(|(_, _, encoded_len)| *encoded_len)
523 .ok_or(WalWriteError::Poisoned("non-empty WAL batch"))?;
524 let first_required = state
525 .pending_groups
526 .back()
527 .map(|group| group.encoded_bytes.saturating_add(first_encoded_len))
528 .ok_or(WalWriteError::Poisoned("pending WAL group"))?;
529 if state
530 .pending_groups
531 .back()
532 .is_some_and(|group| group.records == 0 && first_required > self.max_group_bytes)
533 {
534 return Err(WalWriteError::GroupTooLarge {
535 encoded_bytes: first_required,
536 max_group_bytes: self.max_group_bytes,
537 });
538 }
539 let count = u64::try_from(records.len()).map_err(|_| WalWriteError::SequenceExhausted)?;
540 let start = state.next_seq;
541 let end = start
542 .checked_add(count)
543 .ok_or(WalWriteError::SequenceExhausted)?;
544 let mut seq = start;
545 let storage = Arc::get_mut(&mut encoded)
546 .ok_or(WalWriteError::Poisoned("unique preallocated WAL batch"))?;
547 for (op, payload, _) in &encoded_lengths {
548 encode(
549 WalRecord {
550 seq: LogSeq::new(seq),
551 op: *op,
552 payload,
553 },
554 storage,
555 )
556 .map_err(WalWriteError::Record)?;
557 seq = seq.saturating_add(1);
558 }
559
560 seq = start;
561 let mut offset = 0_usize;
562 let mut chunk_start = 0_usize;
563 for (op, _, encoded_len) in encoded_lengths {
564 let record_seq = LogSeq::new(seq);
565 let record_start = offset;
566 let record_end = offset.saturating_add(encoded_len);
567 while offset < record_end {
568 let available = self
569 .max_group_bytes
570 .saturating_sub(Self::current_group_ref(&state)?.encoded_bytes);
571 if available == 0 {
572 if chunk_start < offset {
573 let group = Self::current_group(&mut state)?;
574 group.chunks.push(EncodedChunk {
575 encoded: Arc::clone(&encoded),
576 range: chunk_start..offset,
577 });
578 }
579 Self::push_empty_pending_group(&mut state);
580 chunk_start = offset;
581 continue;
582 }
583 let written = available.min(record_end.saturating_sub(offset));
584 let group = Self::current_group(&mut state)?;
585 group.encoded_bytes = group.encoded_bytes.saturating_add(written);
586 offset = offset.saturating_add(written);
587 }
588 let group = Self::current_group(&mut state)?;
589 group.records = group.records.saturating_add(1);
590 group.last_seq = Some(record_seq);
591 state.retained_bytes = state.retained_bytes.saturating_add(encoded_len);
592 state.visible.push_back(VisibleRecord::writer_owned(
593 record_seq,
594 op,
595 Arc::clone(&encoded),
596 record_start..record_end,
597 ));
598 seq = seq.saturating_add(1);
599 }
600 let group = Self::current_group(&mut state)?;
601 group.chunks.push(EncodedChunk {
602 encoded,
603 range: chunk_start..offset,
604 });
605 state.next_seq = end;
606 state.encoded_buffer_allocations = state.encoded_buffer_allocations.saturating_add(1);
607 let leader = !state.barrier_in_flight;
608 if leader {
609 state.barrier_in_flight = true;
610 }
611 Ok((LogSeq::new(start)..LogSeq::new(end), leader))
612 }
613
614 #[inline(always)]
615 fn current_group(state: &mut WriterState) -> Result<&mut Group, WalWriteError> {
616 state
617 .pending_groups
618 .back_mut()
619 .ok_or(WalWriteError::Poisoned("pending WAL group"))
620 }
621
622 #[inline(always)]
623 fn current_group_ref(state: &WriterState) -> Result<&Group, WalWriteError> {
624 state
625 .pending_groups
626 .back()
627 .ok_or(WalWriteError::Poisoned("pending WAL group"))
628 }
629
630 fn ensure_pending_group(state: &mut WriterState) {
631 if state.pending_groups.is_empty() {
632 Self::push_empty_pending_group(state);
633 }
634 }
635
636 fn push_empty_pending_group(state: &mut WriterState) {
637 let chunks = std::mem::take(&mut state.spare_chunks);
638 state.pending_groups.push_back(Group {
639 chunks,
640 encoded_bytes: 0,
641 records: 0,
642 last_seq: None,
643 });
644 }
645
646 pub(crate) fn poison(&self, error: &std::io::Error) -> Result<(), WalWriteError> {
650 let mut state = self.lock_state()?;
651 state.failure.get_or_insert_with(|| {
652 Arc::new(Failure {
653 kind: error.kind(),
654 detail: Arc::from(error.to_string()),
655 })
656 });
657 self.changed.notify_all();
658 Ok(())
659 }
660
661 pub fn flush(&self) -> Result<(), WalWriteError> {
666 let (target, leader) = {
667 let mut state = self.lock_state()?;
668 if let Some(failure) = &state.failure {
669 return Err(WalWriteError::failed(failure));
670 }
671 let Some(target) = state.visible.back().map(|record| record.seq) else {
672 return Ok(());
673 };
674 if state
675 .durable_end
676 .is_some_and(|durable_end| durable_end >= target)
677 {
678 return Ok(());
679 }
680 let leader = if state.barrier_in_flight {
681 false
682 } else if state.pending_groups.iter().any(|group| group.records != 0) {
683 state.barrier_in_flight = true;
684 true
685 } else {
686 return Err(WalWriteError::Poisoned(
687 "non-durable WAL record has neither a pending group nor a leader",
688 ));
689 };
690 (target, leader)
691 };
692 if leader {
693 self.flush_as_leader()?;
694 }
695 self.wait_until_durable(target)
696 }
697
698 pub fn visible_records(
703 &self,
704 first_seq: LogSeq,
705 limit: usize,
706 ) -> Result<Vec<VisibleRecord>, WalWriteError> {
707 let mut visible = Vec::with_capacity(limit);
708 let state = self.lock_state()?;
709 visible.extend(
710 state
711 .visible
712 .iter()
713 .filter(|record| record.seq >= first_seq)
714 .take(limit)
715 .cloned(),
716 );
717 Ok(visible)
718 }
719
720 pub fn retire_visible_through(
726 &self,
727 absorbed_through: LogSeq,
728 ) -> Result<WalRetirement, WalRetireError> {
729 let capacity = {
730 let state = self.lock_state().map_err(WalRetireError::Writer)?;
731 if state
732 .durable_end
733 .is_none_or(|durable_end| absorbed_through > durable_end)
734 {
735 return Err(WalRetireError::BeyondDurable {
736 requested: absorbed_through,
737 durable_end: state.durable_end,
738 });
739 }
740 state
741 .visible
742 .iter()
743 .take_while(|record| record.seq <= absorbed_through)
744 .count()
745 };
746 let mut retired = Vec::with_capacity(capacity);
747 let retirement = {
748 let mut state = self.lock_state().map_err(WalRetireError::Writer)?;
749 if state
750 .durable_end
751 .is_none_or(|durable_end| absorbed_through > durable_end)
752 {
753 return Err(WalRetireError::BeyondDurable {
754 requested: absorbed_through,
755 durable_end: state.durable_end,
756 });
757 }
758 while state
759 .visible
760 .front()
761 .is_some_and(|record| record.seq <= absorbed_through)
762 {
763 let Some(record) = state.visible.pop_front() else {
764 break;
765 };
766 state.retained_bytes = state.retained_bytes.saturating_sub(record.encoded_len());
767 retired.push(record);
768 }
769 WalRetirement {
770 absorbed_through,
771 records_released: retired.len(),
772 encoded_bytes_released: retired.iter().fold(0_usize, |total, record| {
773 total.saturating_add(record.encoded_len())
774 }),
775 retained_records: state.visible.len(),
776 retained_bytes: state.retained_bytes,
777 durable_end: state.durable_end,
778 }
779 };
780 drop(retired);
781 Ok(retirement)
782 }
783
784 pub fn stats(&self) -> Result<WalWriterStats, WalWriteError> {
786 let mut recent_groups = Vec::with_capacity(RECENT_GROUP_LIMIT);
787 let state = self.lock_state()?;
788 recent_groups.extend(state.recent_groups.iter().copied());
789 let pending_records = state
790 .pending_groups
791 .iter()
792 .fold(0_usize, |total, group| total.saturating_add(group.records));
793 Ok(WalWriterStats {
794 completed_groups: state.completed_groups,
795 completed_records: state.completed_records,
796 completed_bytes: state.completed_bytes,
797 group_size_histogram: state.group_size_histogram,
798 recent_groups,
799 pending_records,
800 visible_end: state.visible.back().map(|record| record.seq),
801 durable_end: state.durable_end,
802 retained_records: state.visible.len(),
803 retained_bytes: state.retained_bytes,
804 encoded_buffer_allocations: state.encoded_buffer_allocations,
805 })
806 }
807
808 fn stage(&self, op: u16, payload: &[u8]) -> Result<(LogSeq, bool), WalWriteError> {
809 let encoded_len = encoded_record_len(payload.len()).map_err(WalWriteError::Record)?;
810 let mut encoded = Arc::new(Vec::with_capacity(encoded_len));
811 let mut state = self.lock_state()?;
812 loop {
813 if let Some(failure) = &state.failure {
814 return Err(WalWriteError::failed(failure));
815 }
816 Self::ensure_pending_group(&mut state);
817 let seq = LogSeq::new(state.next_seq);
818 let (required, pending_records) = state
819 .pending_groups
820 .back()
821 .map(|group| {
822 (
823 group.encoded_bytes.saturating_add(encoded_len),
824 group.records,
825 )
826 })
827 .ok_or(WalWriteError::Poisoned("pending WAL group"))?;
828 if required <= self.max_group_bytes {
829 let next_seq = state
830 .next_seq
831 .checked_add(1)
832 .ok_or(WalWriteError::SequenceExhausted)?;
833 let storage = Arc::get_mut(&mut encoded)
834 .ok_or(WalWriteError::Poisoned("unique preallocated WAL record"))?;
835 encode_record_into(WalRecord { seq, op, payload }, storage)
836 .map_err(WalWriteError::Record)?;
837 state.next_seq = next_seq;
838 state.encoded_buffer_allocations =
839 state.encoded_buffer_allocations.saturating_add(1);
840 let group = state
841 .pending_groups
842 .back_mut()
843 .ok_or(WalWriteError::Poisoned("pending WAL group"))?;
844 group.chunks.push(EncodedChunk {
845 encoded: Arc::clone(&encoded),
846 range: 0..encoded_len,
847 });
848 group.encoded_bytes = required;
849 group.records = group.records.saturating_add(1);
850 group.last_seq = Some(seq);
851 state.retained_bytes = state.retained_bytes.saturating_add(encoded_len);
852 state.visible.push_back(VisibleRecord::writer_owned(
853 seq,
854 op,
855 encoded,
856 0..encoded_len,
857 ));
858 let leader = !state.barrier_in_flight;
859 if leader {
860 state.barrier_in_flight = true;
861 }
862 return Ok((seq, leader));
863 }
864 if pending_records == 0 {
865 return Err(WalWriteError::GroupTooLarge {
866 encoded_bytes: required,
867 max_group_bytes: self.max_group_bytes,
868 });
869 }
870 state = self
871 .changed
872 .wait(state)
873 .map_err(|_| WalWriteError::Poisoned("WAL state mutex"))?;
874 }
875 }
876
877 fn flush_as_leader(&self) -> Result<(), WalWriteError> {
878 loop {
879 let (group, last_seq) = {
880 let mut state = self.lock_state()?;
881 let group = state
882 .pending_groups
883 .pop_front()
884 .ok_or(WalWriteError::Poisoned("WAL leader found no pending group"))?;
885 let last_seq = group.last_seq.ok_or(WalWriteError::Poisoned(
886 "WAL leader found no pending sequence",
887 ))?;
888 self.changed.notify_all();
889 (group, last_seq)
890 };
891 let failure = self.write_group(&group).err().map(|error| {
892 Arc::new(Failure {
893 kind: error.kind(),
894 detail: Arc::from(error.to_string()),
895 })
896 });
897 let completed = CompletedGroup {
898 records: group.records,
899 encoded_bytes: group.encoded_bytes,
900 };
901 let mut reusable_chunks = group.chunks;
902 reusable_chunks.clear();
903 let mut state = self.lock_state()?;
904 state.spare_chunks = reusable_chunks;
905 match failure {
906 None => {
907 state.durable_end = Some(last_seq);
908 self.durable_progress
909 .store(last_seq.get(), Ordering::Release);
910 state.completed_groups = state.completed_groups.saturating_add(1);
911 state.completed_records = state
912 .completed_records
913 .saturating_add(completed.records as u64);
914 state.completed_bytes = state
915 .completed_bytes
916 .saturating_add(completed.encoded_bytes as u64);
917 state.group_size_histogram.record(completed.records);
918 if state.recent_groups.len() == RECENT_GROUP_LIMIT {
919 state.recent_groups.pop_front();
920 }
921 state.recent_groups.push_back(completed);
922 self.changed.notify_all();
923 if state.pending_groups.is_empty() {
924 state.barrier_in_flight = false;
925 return Ok(());
926 }
927 }
928 Some(failure) => {
929 state.failure = Some(Arc::clone(&failure));
930 state.barrier_in_flight = false;
931 self.changed.notify_all();
932 return Err(WalWriteError::failed(&failure));
933 }
934 }
935 }
936 }
937
938 #[cfg(feature = "graph-cypher")]
939 pub(crate) fn io_work(&self) -> [u64; 4] {
940 std::array::from_fn(|index| {
941 self.io_work
942 .get(index)
943 .map_or(0, |counter| counter.load(Ordering::Relaxed))
944 })
945 }
946
947 #[cfg(feature = "graph-cypher")]
949 pub(crate) fn atomic_run_bound(&self) -> Result<(usize, usize), WalWriteError> {
950 let state = self.lock_state()?;
951 let pending = state.pending_groups.iter().fold(0_usize, |sum, group| {
952 sum.saturating_add(group.encoded_bytes)
953 });
954 Ok((pending, self.max_group_bytes))
955 }
956
957 fn write_group(&self, group: &Group) -> std::io::Result<()> {
958 let mut buffers = Vec::with_capacity(group.chunks.len());
959 for chunk in &group.chunks {
960 buffers.push(IoSlice::new(chunk.bytes()?));
961 }
962 let mut file = self
963 .file
964 .lock()
965 .map_err(|_| std::io::Error::other("WAL file mutex poisoned"))?;
966 if let Some(counter) = self.io_work.first() {
967 counter.fetch_add(1, Ordering::Relaxed);
968 }
969 file.append_vectored(&mut buffers)?;
970 if let Some(counter) = self.io_work.get(1) {
971 counter.fetch_add(group.encoded_bytes as u64, Ordering::Relaxed);
972 }
973 match self.sync {
974 SyncRequirement::Skip => Ok(()),
975 SyncRequirement::Sync(kind) => {
976 if kind == SyncKind::Full
977 && let Some(counter) = self.io_work.get(2)
978 {
979 counter.fetch_add(1, Ordering::Relaxed);
980 }
981 let result = file.sync(kind);
982 if kind == SyncKind::Full
983 && result.is_ok()
984 && let Some(counter) = self.io_work.get(3)
985 {
986 counter.fetch_add(1, Ordering::Relaxed);
987 }
988 result
989 }
990 }?;
991 drop(file);
992 let mut created_directory_sync = self
993 .created_directory_sync
994 .lock()
995 .map_err(|_| std::io::Error::other("WAL directory-sync mutex poisoned"))?;
996 if let Some(sync) = created_directory_sync.as_ref() {
997 sync.vfs.sync(&sync.directory, sync.kind)?;
998 created_directory_sync.take();
999 }
1000 Ok(())
1001 }
1002
1003 fn wait_until_durable(&self, seq: LogSeq) -> Result<(), WalWriteError> {
1004 let mut state = self.lock_state()?;
1005 while state
1006 .durable_end
1007 .is_none_or(|durable_end| durable_end < seq)
1008 {
1009 if let Some(failure) = &state.failure {
1010 return Err(WalWriteError::failed(failure));
1011 }
1012 state = self
1013 .changed
1014 .wait(state)
1015 .map_err(|_| WalWriteError::Poisoned("WAL state mutex"))?;
1016 }
1017 Ok(())
1018 }
1019
1020 fn lock_state(&self) -> Result<MutexGuard<'_, WriterState>, WalWriteError> {
1021 self.state
1022 .lock()
1023 .map_err(|_| WalWriteError::Poisoned("WAL state mutex"))
1024 }
1025}
1026
1027pub(crate) fn encode_wal_image(
1028 first_seq: LogSeq,
1029 records: &[(u16, Vec<u8>)],
1030) -> Result<Vec<u8>, WalWriteError> {
1031 let mut encoded = header::encode_header(first_seq).map_err(WalWriteError::Header)?;
1032 let mut sequence = first_seq.get();
1033 for (op, payload) in records {
1034 record::append_record_into(
1035 record::WalRecord {
1036 seq: LogSeq::new(sequence),
1037 op: *op,
1038 payload,
1039 },
1040 &mut encoded,
1041 )
1042 .map_err(WalWriteError::Record)?;
1043 sequence = sequence
1044 .checked_add(1)
1045 .ok_or(WalWriteError::SequenceExhausted)?;
1046 }
1047 Ok(encoded)
1048}
1049
1050fn default_max_group_bytes(policy: DurabilityPolicy) -> usize {
1051 match policy.data_file_sync() {
1052 SyncRequirement::Sync(SyncKind::Full) => DEFAULT_MAX_GROUP_BYTES_DURABLE,
1053 SyncRequirement::Skip | SyncRequirement::Sync(SyncKind::Barrier) => DEFAULT_MAX_GROUP_BYTES,
1054 }
1055}
1056
1057#[cfg(test)]
1058#[allow(clippy::expect_used)]
1059mod tests {
1060 use std::path::Path;
1061
1062 use super::{LogSeq, RecordEncodeError, WalWriteError, WalWriter, append_record_into};
1063 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
1064 use crate::vfs::CountingVfs;
1065 use crate::vfs::crash::MemoryVfs;
1066
1067 #[test]
1068 fn durable_policy_defaults_to_a_sixteen_mib_group_cap() {
1069 let counting = CountingVfs::new(MemoryVfs::new());
1070 let policy = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::Durable)
1071 .expect("durable policy");
1072 let writer = WalWriter::create(
1073 &counting,
1074 Path::new("/wal/default-durable-cap.ze"),
1075 LogSeq::new(1),
1076 policy,
1077 )
1078 .expect("writer");
1079 let payload = vec![0x5a; 1_024];
1080 let records = (0..1_024)
1081 .map(|_| (7, payload.as_slice()))
1082 .collect::<Vec<_>>();
1083
1084 writer.commit_many(&records).expect("large durable batch");
1085 let stats = writer.stats().expect("writer stats");
1086
1087 assert_eq!(
1088 (
1089 counting.append_calls(),
1090 counting.handle_full_sync_calls(),
1091 stats.completed_groups,
1092 stats.completed_records,
1093 ),
1094 (1, 1, 1, 1_024),
1095 "a 1,071,144-byte durable batch must share one append and full sync"
1096 );
1097 }
1098
1099 #[test]
1100 fn ordered_and_skip_policies_keep_the_one_mib_default() {
1101 let payload = vec![0x6b; 1_024];
1102 let records = (0..1_024)
1103 .map(|_| (8, payload.as_slice()))
1104 .collect::<Vec<_>>();
1105 let mut actual = Vec::new();
1106
1107 for (tier, path) in [
1108 (CommitTier::Ordered, "/wal/default-ordered-cap.ze"),
1109 (CommitTier::None, "/wal/default-skip-cap.ze"),
1110 ] {
1111 let counting = CountingVfs::new(MemoryVfs::new());
1112 let policy =
1113 DurabilityPolicy::new(DurabilityMode::Durable, tier).expect("supported policy");
1114 let writer = WalWriter::create(&counting, Path::new(path), LogSeq::new(1), policy)
1115 .expect("writer");
1116
1117 writer.commit_many(&records).expect("large batch");
1118 let stats = writer.stats().expect("writer stats");
1119 actual.push((
1120 tier,
1121 counting.append_calls(),
1122 counting.handle_barrier_sync_calls(),
1123 counting.handle_full_sync_calls(),
1124 stats.completed_groups,
1125 ));
1126 }
1127
1128 assert_eq!(
1129 actual,
1130 vec![
1131 (CommitTier::Ordered, 2, 2, 0, 2),
1132 (CommitTier::None, 2, 0, 0, 2),
1133 ],
1134 "ordered and skip policies must split a 1,071,144-byte batch at one MiB"
1135 );
1136 }
1137
1138 #[test]
1139 fn an_explicit_group_cap_still_wins() {
1140 let counting = CountingVfs::new(MemoryVfs::new());
1141 let policy = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::Durable)
1142 .expect("durable policy");
1143 let writer = WalWriter::create_with_max_group_bytes(
1144 &counting,
1145 Path::new("/wal/explicit-group-cap.ze"),
1146 LogSeq::new(1),
1147 policy,
1148 128,
1149 )
1150 .expect("writer");
1151 writer.commit(0, &[0; 10]).expect("flush WAL header");
1152 let groups_before = writer.stats().expect("prime stats").recent_groups.len();
1153 counting.reset();
1154 let payload = [0x7c; 10];
1155 let records = (0..12).map(|_| (9, payload.as_slice())).collect::<Vec<_>>();
1156
1157 writer.commit_many(&records).expect("explicit-cap batch");
1158 let stats = writer.stats().expect("batch stats");
1159 let groups = stats
1160 .recent_groups
1161 .iter()
1162 .skip(groups_before)
1163 .map(|group| (group.records, group.encoded_bytes))
1164 .collect::<Vec<_>>();
1165
1166 assert_eq!(
1167 (
1168 counting.append_calls(),
1169 counting.handle_full_sync_calls(),
1170 groups,
1171 ),
1172 (3, 3, vec![(4, 128), (4, 128), (4, 128)]),
1173 "an explicit 128-byte cap must override the durable default"
1174 );
1175 }
1176
1177 #[test]
1178 fn encoding_failure_in_middle_of_commit_many_is_atomic() {
1179 let policy = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::None)
1180 .expect("supported policy");
1181 let writer = WalWriter::create(
1182 &MemoryVfs::new(),
1183 Path::new("/wal/unit-encode-failure.ze"),
1184 LogSeq::new(1),
1185 policy,
1186 )
1187 .expect("writer");
1188 let records = [(51, b"first".as_slice()), (52, b"second".as_slice())];
1189 let mut encode_calls = 0_usize;
1190
1191 let result = writer.stage_many_with_encoder(&records, |record, storage| {
1192 encode_calls = encode_calls.saturating_add(1);
1193 if encode_calls == 2 {
1194 Err(RecordEncodeError::PayloadTooLarge(usize::MAX))
1195 } else {
1196 append_record_into(record, storage)
1197 }
1198 });
1199 let failure = match result {
1200 Err(WalWriteError::Record(error)) => format!("error={error}"),
1201 other => format!("unexpected={other:?}"),
1202 };
1203 let visible = writer
1204 .visible_records(LogSeq::new(1), records.len())
1205 .expect("visibility after failed encoding")
1206 .len();
1207 let next = writer
1208 .commit(53, b"after")
1209 .expect("sequence remains reusable");
1210
1211 assert_eq!(
1212 (failure, visible, next),
1213 (
1214 format!("error=WAL payload length {} exceeds u32", usize::MAX),
1215 0,
1216 LogSeq::new(1),
1217 ),
1218 "a failure encoding any batch record must consume no sequence and publish no visibility"
1219 );
1220 }
1221}
1222
1223impl DurableLog for WalWriter {
1224 fn durable_end(&self) -> u64 {
1225 self.durable_progress.load(Ordering::Acquire)
1226 }
1227}
1228
1229#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1231pub enum VisibleRecordError {
1232 InvalidEncodedRange {
1234 start: usize,
1236 end: usize,
1238 available: usize,
1240 },
1241 Record(RecordError),
1243}
1244
1245impl std::fmt::Display for VisibleRecordError {
1246 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1247 match self {
1248 Self::InvalidEncodedRange {
1249 start,
1250 end,
1251 available,
1252 } => write!(
1253 formatter,
1254 "visible WAL encoded range {start}..{end} exceeds {available} bytes"
1255 ),
1256 Self::Record(error) => write!(formatter, "visible WAL record: {error}"),
1257 }
1258 }
1259}
1260
1261impl std::error::Error for VisibleRecordError {
1262 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1263 match self {
1264 Self::InvalidEncodedRange { .. } => None,
1265 Self::Record(error) => Some(error),
1266 }
1267 }
1268}
1269
1270#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1272pub struct WalRetirement {
1273 pub absorbed_through: LogSeq,
1275 pub records_released: usize,
1277 pub encoded_bytes_released: usize,
1279 pub retained_records: usize,
1281 pub retained_bytes: usize,
1283 pub durable_end: Option<LogSeq>,
1285}
1286
1287#[derive(Debug)]
1289pub enum WalRetireError {
1290 BeyondDurable {
1292 requested: LogSeq,
1294 durable_end: Option<LogSeq>,
1296 },
1297 Writer(WalWriteError),
1299}
1300
1301impl std::fmt::Display for WalRetireError {
1302 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1303 match self {
1304 Self::BeyondDurable {
1305 requested,
1306 durable_end: Some(durable_end),
1307 } => write!(
1308 formatter,
1309 "cannot retire WAL sequence {} past durable end {}",
1310 requested.get(),
1311 durable_end.get()
1312 ),
1313 Self::BeyondDurable {
1314 requested,
1315 durable_end: None,
1316 } => write!(
1317 formatter,
1318 "cannot retire WAL sequence {} before any sequence is durable",
1319 requested.get()
1320 ),
1321 Self::Writer(error) => write!(formatter, "cannot retire visible WAL records: {error}"),
1322 }
1323 }
1324}
1325
1326impl std::error::Error for WalRetireError {
1327 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1328 match self {
1329 Self::BeyondDurable { .. } => None,
1330 Self::Writer(error) => Some(error),
1331 }
1332 }
1333}
1334
1335#[derive(Clone, Debug, Eq, PartialEq)]
1337pub struct WalReader {
1338 records: Vec<VisibleRecord>,
1339 next_seq: u64,
1340 durable_end: u64,
1341 terminator: Option<ReplayTerminator>,
1342}
1343
1344pub(crate) struct CleanWalReader {
1345 records: Vec<VisibleRecord>,
1346 next_seq: u64,
1347 durable_end: Option<LogSeq>,
1348}
1349
1350impl CleanWalReader {
1351 pub(crate) fn durable_end(&self) -> u64 {
1352 self.durable_end.map_or(0, LogSeq::get)
1353 }
1354
1355 #[cfg(feature = "graph-cypher")]
1356 pub(crate) fn retained_first_seq(&self) -> u64 {
1357 self.records
1358 .first()
1359 .map_or(self.next_seq, |record| record.seq.get())
1360 }
1361
1362 pub(crate) fn records(&self) -> &[VisibleRecord] {
1363 &self.records
1364 }
1365}
1366
1367impl WalReader {
1368 pub fn open(vfs: &dyn Vfs, path: &Path) -> Result<Self, WalReadError> {
1370 let bytes = match vfs.read(path) {
1371 Ok(bytes) => bytes,
1372 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1373 return Ok(Self {
1374 records: Vec::new(),
1375 next_seq: 0,
1376 durable_end: 0,
1377 terminator: None,
1378 });
1379 }
1380 Err(error) => return Err(WalReadError::Io(error)),
1381 };
1382 let replayed = replay(&bytes);
1383 let terminator = replayed.terminator;
1384 if matches!(terminator, ReplayTerminator::InvalidHeader(_)) {
1385 return Ok(Self {
1386 records: Vec::new(),
1387 next_seq: 0,
1388 durable_end: 0,
1389 terminator: Some(terminator),
1390 });
1391 }
1392 let header = header::decode_header(&bytes).map_err(WalReadError::Header)?;
1393 let mut offset = header::WAL_HEADER_LEN;
1394 let recovered = replayed
1395 .records
1396 .iter()
1397 .map(|record| {
1398 let encoded_len = record::MIN_RECORD_LEN.saturating_add(record.payload.len());
1399 let range = offset..offset.saturating_add(encoded_len);
1400 offset = range.end;
1401 (record.seq, record.op, range)
1402 })
1403 .collect::<Vec<_>>();
1404 drop(replayed);
1405 let records = recovered
1406 .into_iter()
1407 .map(|(seq, op, range)| {
1408 let start = range.start;
1409 let end = range.end;
1410 let encoded = bytes
1411 .get(range)
1412 .ok_or(WalReadError::InvalidRecoveredRange {
1413 start,
1414 end,
1415 available: bytes.len(),
1416 })?;
1417 let encoded = Arc::new(encoded.to_vec());
1418 let length = encoded.len();
1419 Ok(VisibleRecord::recovered(seq, op, encoded, 0..length))
1420 })
1421 .collect::<Result<Vec<_>, WalReadError>>()?;
1422 let durable_end = records.last().map_or_else(
1423 || header.first_seq.get().saturating_sub(1),
1424 |record| record.seq.get(),
1425 );
1426 let next_seq = records.last().map_or(header.first_seq.get(), |record| {
1427 record.seq.get().saturating_add(1)
1428 });
1429 Ok(Self {
1430 records,
1431 next_seq,
1432 durable_end,
1433 terminator: Some(terminator),
1434 })
1435 }
1436
1437 #[must_use]
1439 pub fn records(&self) -> &[VisibleRecord] {
1440 &self.records
1441 }
1442
1443 #[must_use]
1445 pub const fn terminator(&self) -> Option<ReplayTerminator> {
1446 self.terminator
1447 }
1448
1449 #[cfg(feature = "graph-cypher")]
1450 pub(crate) fn into_graph_prefix(mut self) -> Result<CleanWalReader, WalRecoveryError> {
1451 if matches!(
1452 self.terminator,
1453 Some(ReplayTerminator::CorruptAt {
1454 reason: replay::CorruptionReason::Record {
1455 location: replay::CorruptionLocation::Tail,
1456 error: RecordError::BodyTruncated { needed, .. },
1457 },
1458 ..
1459 }) if needed <= DEFAULT_MAX_GROUP_BYTES_DURABLE
1460 ) {
1461 self.terminator = Some(ReplayTerminator::CleanEnd);
1462 }
1463 self.into_clean()
1464 }
1465
1466 pub(crate) fn into_clean(self) -> Result<CleanWalReader, WalRecoveryError> {
1467 match self.terminator {
1468 Some(ReplayTerminator::CleanEnd) => {
1469 let durable_end = Some(LogSeq::new(self.durable_end));
1470 Ok(CleanWalReader {
1471 records: self.records,
1472 next_seq: self.next_seq,
1473 durable_end,
1474 })
1475 }
1476 Some(ReplayTerminator::InvalidHeader(error)) => {
1477 let error = WalRecoveryError::InvalidHeader(error);
1478 #[cfg(any(test, feature = "test-seams"))]
1479 crate::lifecycle::record_storage_wal_recovery_fault(&error);
1480 Err(error)
1481 }
1482 Some(ReplayTerminator::CorruptAt { offset, reason }) => {
1483 let error = WalRecoveryError::CorruptAt { offset, reason };
1484 #[cfg(any(test, feature = "test-seams"))]
1485 crate::lifecycle::record_storage_wal_recovery_fault(&error);
1486 Err(error)
1487 }
1488 None => Err(WalRecoveryError::MissingFile),
1489 }
1490 }
1491}
1492
1493impl DurableLog for WalReader {
1494 fn durable_end(&self) -> u64 {
1495 self.durable_end
1496 }
1497}
1498
1499#[derive(Debug)]
1501pub enum WalWriteError {
1502 NonEmptyWal {
1504 length: u64,
1506 },
1507 RecoveredBytesOverflow,
1509 GroupTooLarge {
1511 encoded_bytes: usize,
1513 max_group_bytes: usize,
1515 },
1516 SequenceExhausted,
1518 Header(RegistryError),
1520 Record(RecordEncodeError),
1522 Poisoned(&'static str),
1524 Failed {
1526 kind: std::io::ErrorKind,
1528 detail: Arc<str>,
1530 },
1531}
1532
1533impl WalWriteError {
1534 fn from_io(error: std::io::Error) -> Self {
1535 Self::Failed {
1536 kind: error.kind(),
1537 detail: Arc::from(error.to_string()),
1538 }
1539 }
1540
1541 fn failed(failure: &Failure) -> Self {
1542 Self::Failed {
1543 kind: failure.kind,
1544 detail: Arc::clone(&failure.detail),
1545 }
1546 }
1547}
1548
1549impl std::fmt::Display for WalWriteError {
1550 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1551 match self {
1552 Self::NonEmptyWal { length } => {
1553 write!(formatter, "WAL path is not empty: {length} bytes")
1554 }
1555 Self::RecoveredBytesOverflow => {
1556 formatter.write_str("recovered WAL record bytes overflow usize")
1557 }
1558 Self::GroupTooLarge {
1559 encoded_bytes,
1560 max_group_bytes,
1561 } => write!(
1562 formatter,
1563 "WAL group needs {encoded_bytes} bytes, exceeding {max_group_bytes}"
1564 ),
1565 Self::SequenceExhausted => formatter.write_str("WAL sequence space exhausted"),
1566 Self::Header(error) => write!(formatter, "WAL header: {error}"),
1567 Self::Record(error) => write!(formatter, "WAL record: {error}"),
1568 Self::Poisoned(detail) => write!(formatter, "{detail} poisoned"),
1569 Self::Failed { kind, detail } => {
1570 write!(formatter, "WAL writer failed with {:?}: {}", kind, detail)
1571 }
1572 }
1573 }
1574}
1575
1576impl std::error::Error for WalWriteError {}
1577
1578#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1580pub enum WalRecoveryError {
1581 MissingFile,
1583 InvalidHeader(header::WalHeaderError),
1585 CorruptAt {
1587 offset: usize,
1589 reason: replay::CorruptionReason,
1591 },
1592}
1593
1594impl std::fmt::Display for WalRecoveryError {
1595 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1596 match self {
1597 Self::MissingFile => formatter.write_str("WAL recovery path is missing"),
1598 Self::InvalidHeader(error) => write!(formatter, "WAL recovery header: {error}"),
1599 Self::CorruptAt { offset, reason } => {
1600 write!(
1601 formatter,
1602 "WAL recovery stopped at byte {offset}: {reason:?}"
1603 )
1604 }
1605 }
1606 }
1607}
1608
1609impl std::error::Error for WalRecoveryError {
1610 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1611 match self {
1612 Self::InvalidHeader(error) => Some(error),
1613 Self::MissingFile | Self::CorruptAt { .. } => None,
1614 }
1615 }
1616}
1617
1618#[derive(Debug)]
1620pub enum WalReadError {
1621 Io(std::io::Error),
1623 Header(header::WalHeaderError),
1625 InvalidRecoveredRange {
1627 start: usize,
1629 end: usize,
1631 available: usize,
1633 },
1634}
1635
1636impl std::fmt::Display for WalReadError {
1637 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1638 match self {
1639 Self::Io(error) => write!(formatter, "WAL read: {error}"),
1640 Self::Header(error) => write!(formatter, "WAL read header: {error}"),
1641 Self::InvalidRecoveredRange {
1642 start,
1643 end,
1644 available,
1645 } => write!(
1646 formatter,
1647 "WAL recovered range {start}..{end} exceeds {available} bytes"
1648 ),
1649 }
1650 }
1651}
1652
1653impl std::error::Error for WalReadError {
1654 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1655 match self {
1656 Self::Io(error) => Some(error),
1657 Self::Header(error) => Some(error),
1658 Self::InvalidRecoveredRange { .. } => None,
1659 }
1660 }
1661}