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}
256
257struct CreatedDirectorySync {
258 vfs: Arc<dyn Vfs>,
259 directory: PathBuf,
260 kind: SyncKind,
261}
262
263impl WalWriter {
264 pub fn create(
269 vfs: &dyn Vfs,
270 path: &Path,
271 first_seq: LogSeq,
272 policy: DurabilityPolicy,
273 ) -> Result<Self, WalWriteError> {
274 let max_group_bytes = default_max_group_bytes(policy);
275 Self::create_with_max_group_bytes(vfs, path, first_seq, policy, max_group_bytes)
276 }
277
278 pub(crate) fn resume(
279 vfs: &dyn Vfs,
280 path: &Path,
281 recovered: CleanWalReader,
282 policy: DurabilityPolicy,
283 ) -> Result<Self, WalWriteError> {
284 let max_group_bytes = default_max_group_bytes(policy);
285 let next_seq = recovered.next_seq;
286 let durable_end = recovered.durable_end;
287 let durable_progress = durable_end.map_or(0, LogSeq::get);
288 let visible = VecDeque::from(recovered.records);
289 let retained_bytes = visible.iter().try_fold(0_usize, |total, record| {
290 total
291 .checked_add(record.encoded_len())
292 .ok_or(WalWriteError::RecoveredBytesOverflow)
293 })?;
294 let file = vfs.open_append(path).map_err(WalWriteError::from_io)?;
295 Ok(Self {
296 file: Mutex::new(file),
297 created_directory_sync: Mutex::new(None),
298 state: Mutex::new(WriterState {
299 next_seq,
300 visible,
301 retained_bytes,
302 durable_end,
303 pending_groups: VecDeque::with_capacity(2),
304 spare_chunks: Vec::with_capacity(16),
305 barrier_in_flight: false,
306 failure: None,
307 completed_groups: 0,
308 completed_records: 0,
309 completed_bytes: 0,
310 encoded_buffer_allocations: 0,
311 group_size_histogram: GroupSizeHistogram::new(),
312 recent_groups: VecDeque::with_capacity(RECENT_GROUP_LIMIT),
313 }),
314 changed: Condvar::new(),
315 sync: policy.data_file_sync(),
316 max_group_bytes,
317 durable_progress: AtomicU64::new(durable_progress),
318 })
319 }
320
321 pub fn create_with_max_group_bytes(
323 vfs: &dyn Vfs,
324 path: &Path,
325 first_seq: LogSeq,
326 policy: DurabilityPolicy,
327 max_group_bytes: usize,
328 ) -> Result<Self, WalWriteError> {
329 match vfs.open(path) {
330 Ok(0) => {}
331 Ok(length) => return Err(WalWriteError::NonEmptyWal { length }),
332 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
333 Err(error) => return Err(WalWriteError::from_io(error)),
334 }
335 let header = encode_header(first_seq).map_err(WalWriteError::Header)?;
336 if max_group_bytes < header.len() {
337 return Err(WalWriteError::GroupTooLarge {
338 encoded_bytes: header.len(),
339 max_group_bytes,
340 });
341 }
342 let file = vfs.open_append(path).map_err(WalWriteError::from_io)?;
343 let header_len = header.len();
344 let header = Arc::new(header);
345 let mut header_chunks = Vec::with_capacity(16);
346 header_chunks.push(EncodedChunk {
347 range: 0..header_len,
348 encoded: header,
349 });
350 let mut pending_groups = VecDeque::with_capacity(2);
351 pending_groups.push_back(Group {
352 chunks: header_chunks,
353 encoded_bytes: header_len,
354 records: 0,
355 last_seq: None,
356 });
357 let spare_chunks = Vec::with_capacity(16);
358 let recent_groups = VecDeque::with_capacity(RECENT_GROUP_LIMIT);
359 Ok(Self {
360 file: Mutex::new(file),
361 created_directory_sync: Mutex::new(None),
362 state: Mutex::new(WriterState {
363 next_seq: first_seq.get(),
364 visible: VecDeque::new(),
365 retained_bytes: 0,
366 durable_end: None,
367 pending_groups,
368 spare_chunks,
369 barrier_in_flight: false,
370 failure: None,
371 completed_groups: 0,
372 completed_records: 0,
373 completed_bytes: 0,
374 encoded_buffer_allocations: 0,
375 group_size_histogram: GroupSizeHistogram::new(),
376 recent_groups,
377 }),
378 changed: Condvar::new(),
379 sync: policy.data_file_sync(),
380 max_group_bytes,
381 durable_progress: AtomicU64::new(first_seq.get().saturating_sub(1)),
382 })
383 }
384
385 pub(crate) fn create_store_wal(
386 vfs: Arc<dyn Vfs>,
387 directory: &Path,
388 path: &Path,
389 first_seq: LogSeq,
390 policy: DurabilityPolicy,
391 ) -> Result<Self, WalWriteError> {
392 let mut writer = Self::create(vfs.as_ref(), path, first_seq, policy)?;
393 if let SyncRequirement::Sync(kind) = policy.directory_sync() {
394 writer.created_directory_sync = Mutex::new(Some(CreatedDirectorySync {
395 vfs,
396 directory: directory.to_path_buf(),
397 kind,
398 }));
399 }
400 Ok(writer)
401 }
402
403 pub fn commit(&self, op: u16, payload: &[u8]) -> Result<LogSeq, WalWriteError> {
406 let (seq, leader) = self.stage(op, payload)?;
407 if leader {
408 self.flush_as_leader()?;
409 }
410 Ok(seq)
411 }
412
413 pub fn commit_durable(&self, op: u16, payload: &[u8]) -> Result<LogSeq, WalWriteError> {
415 let (seq, leader) = self.stage(op, payload)?;
416 if leader {
417 self.flush_as_leader()?;
418 }
419 self.wait_until_durable(seq)?;
420 Ok(seq)
421 }
422
423 pub fn commit_many(&self, records: &[(u16, &[u8])]) -> Result<Range<LogSeq>, WalWriteError> {
437 let (range, leader) = self.stage_many(records)?;
438 if leader {
439 self.flush_as_leader()?;
440 }
441 Ok(range)
442 }
443
444 pub fn commit_many_durable(
457 &self,
458 records: &[(u16, &[u8])],
459 ) -> Result<Range<LogSeq>, WalWriteError> {
460 let (range, leader) = self.stage_many(records)?;
461 if leader {
462 self.flush_as_leader()?;
463 }
464 if range.start != range.end {
465 let last = LogSeq::new(range.end.get().saturating_sub(1));
466 self.wait_until_durable(last)?;
467 }
468 Ok(range)
469 }
470
471 fn stage_many(&self, records: &[(u16, &[u8])]) -> Result<(Range<LogSeq>, bool), WalWriteError> {
472 self.stage_many_with_encoder(records, append_record_into)
473 }
474
475 fn stage_many_with_encoder<F>(
476 &self,
477 records: &[(u16, &[u8])],
478 mut encode: F,
479 ) -> Result<(Range<LogSeq>, bool), WalWriteError>
480 where
481 F: for<'a> FnMut(WalRecord<'a>, &mut Vec<u8>) -> Result<(), RecordEncodeError>,
482 {
483 if records.is_empty() {
484 let state = self.lock_state()?;
485 if let Some(failure) = &state.failure {
486 return Err(WalWriteError::failed(failure));
487 }
488 let next = LogSeq::new(state.next_seq);
489 return Ok((next..next, false));
490 }
491 let encoded_lengths = records
492 .iter()
493 .map(|&(op, payload)| {
494 let encoded_len =
495 encoded_record_len(payload.len()).map_err(WalWriteError::Record)?;
496 Ok((op, payload, encoded_len))
497 })
498 .collect::<Result<Vec<_>, WalWriteError>>()?;
499 let batch_bytes = encoded_lengths
500 .iter()
501 .fold(0_usize, |total, record| total.saturating_add(record.2));
502 if let Some((_, _, encoded_bytes)) = encoded_lengths
503 .iter()
504 .find(|(_, _, encoded_bytes)| *encoded_bytes > self.max_group_bytes)
505 {
506 return Err(WalWriteError::GroupTooLarge {
507 encoded_bytes: *encoded_bytes,
508 max_group_bytes: self.max_group_bytes,
509 });
510 }
511 let mut encoded = Arc::new(Vec::with_capacity(batch_bytes));
512 let mut state = self.lock_state()?;
513 if let Some(failure) = &state.failure {
514 return Err(WalWriteError::failed(failure));
515 }
516 Self::ensure_pending_group(&mut state);
517 let first_encoded_len = encoded_lengths
518 .first()
519 .map(|(_, _, encoded_len)| *encoded_len)
520 .ok_or(WalWriteError::Poisoned("non-empty WAL batch"))?;
521 let first_required = state
522 .pending_groups
523 .back()
524 .map(|group| group.encoded_bytes.saturating_add(first_encoded_len))
525 .ok_or(WalWriteError::Poisoned("pending WAL group"))?;
526 if state
527 .pending_groups
528 .back()
529 .is_some_and(|group| group.records == 0 && first_required > self.max_group_bytes)
530 {
531 return Err(WalWriteError::GroupTooLarge {
532 encoded_bytes: first_required,
533 max_group_bytes: self.max_group_bytes,
534 });
535 }
536 let count = u64::try_from(records.len()).map_err(|_| WalWriteError::SequenceExhausted)?;
537 let start = state.next_seq;
538 let end = start
539 .checked_add(count)
540 .ok_or(WalWriteError::SequenceExhausted)?;
541 let mut seq = start;
542 let storage = Arc::get_mut(&mut encoded)
543 .ok_or(WalWriteError::Poisoned("unique preallocated WAL batch"))?;
544 for (op, payload, _) in &encoded_lengths {
545 encode(
546 WalRecord {
547 seq: LogSeq::new(seq),
548 op: *op,
549 payload,
550 },
551 storage,
552 )
553 .map_err(WalWriteError::Record)?;
554 seq = seq.saturating_add(1);
555 }
556
557 seq = start;
558 let mut offset = 0_usize;
559 let mut chunk_start = 0_usize;
560 for (op, _, encoded_len) in encoded_lengths {
561 let record_seq = LogSeq::new(seq);
562 let record_start = offset;
563 let record_end = offset.saturating_add(encoded_len);
564 while offset < record_end {
565 let available = self
566 .max_group_bytes
567 .saturating_sub(Self::current_group_ref(&state)?.encoded_bytes);
568 if available == 0 {
569 if chunk_start < offset {
570 let group = Self::current_group(&mut state)?;
571 group.chunks.push(EncodedChunk {
572 encoded: Arc::clone(&encoded),
573 range: chunk_start..offset,
574 });
575 }
576 Self::push_empty_pending_group(&mut state);
577 chunk_start = offset;
578 continue;
579 }
580 let written = available.min(record_end.saturating_sub(offset));
581 let group = Self::current_group(&mut state)?;
582 group.encoded_bytes = group.encoded_bytes.saturating_add(written);
583 offset = offset.saturating_add(written);
584 }
585 let group = Self::current_group(&mut state)?;
586 group.records = group.records.saturating_add(1);
587 group.last_seq = Some(record_seq);
588 state.retained_bytes = state.retained_bytes.saturating_add(encoded_len);
589 state.visible.push_back(VisibleRecord::writer_owned(
590 record_seq,
591 op,
592 Arc::clone(&encoded),
593 record_start..record_end,
594 ));
595 seq = seq.saturating_add(1);
596 }
597 let group = Self::current_group(&mut state)?;
598 group.chunks.push(EncodedChunk {
599 encoded,
600 range: chunk_start..offset,
601 });
602 state.next_seq = end;
603 state.encoded_buffer_allocations = state.encoded_buffer_allocations.saturating_add(1);
604 let leader = !state.barrier_in_flight;
605 if leader {
606 state.barrier_in_flight = true;
607 }
608 Ok((LogSeq::new(start)..LogSeq::new(end), leader))
609 }
610
611 #[inline(always)]
612 fn current_group(state: &mut WriterState) -> Result<&mut Group, WalWriteError> {
613 state
614 .pending_groups
615 .back_mut()
616 .ok_or(WalWriteError::Poisoned("pending WAL group"))
617 }
618
619 #[inline(always)]
620 fn current_group_ref(state: &WriterState) -> Result<&Group, WalWriteError> {
621 state
622 .pending_groups
623 .back()
624 .ok_or(WalWriteError::Poisoned("pending WAL group"))
625 }
626
627 fn ensure_pending_group(state: &mut WriterState) {
628 if state.pending_groups.is_empty() {
629 Self::push_empty_pending_group(state);
630 }
631 }
632
633 fn push_empty_pending_group(state: &mut WriterState) {
634 let chunks = std::mem::take(&mut state.spare_chunks);
635 state.pending_groups.push_back(Group {
636 chunks,
637 encoded_bytes: 0,
638 records: 0,
639 last_seq: None,
640 });
641 }
642
643 pub(crate) fn poison(&self, error: &std::io::Error) -> Result<(), WalWriteError> {
647 let mut state = self.lock_state()?;
648 state.failure.get_or_insert_with(|| {
649 Arc::new(Failure {
650 kind: error.kind(),
651 detail: Arc::from(error.to_string()),
652 })
653 });
654 self.changed.notify_all();
655 Ok(())
656 }
657
658 pub fn flush(&self) -> Result<(), WalWriteError> {
663 let (target, leader) = {
664 let mut state = self.lock_state()?;
665 if let Some(failure) = &state.failure {
666 return Err(WalWriteError::failed(failure));
667 }
668 let Some(target) = state.visible.back().map(|record| record.seq) else {
669 return Ok(());
670 };
671 if state
672 .durable_end
673 .is_some_and(|durable_end| durable_end >= target)
674 {
675 return Ok(());
676 }
677 let leader = if state.barrier_in_flight {
678 false
679 } else if state.pending_groups.iter().any(|group| group.records != 0) {
680 state.barrier_in_flight = true;
681 true
682 } else {
683 return Err(WalWriteError::Poisoned(
684 "non-durable WAL record has neither a pending group nor a leader",
685 ));
686 };
687 (target, leader)
688 };
689 if leader {
690 self.flush_as_leader()?;
691 }
692 self.wait_until_durable(target)
693 }
694
695 pub fn visible_records(
700 &self,
701 first_seq: LogSeq,
702 limit: usize,
703 ) -> Result<Vec<VisibleRecord>, WalWriteError> {
704 let mut visible = Vec::with_capacity(limit);
705 let state = self.lock_state()?;
706 visible.extend(
707 state
708 .visible
709 .iter()
710 .filter(|record| record.seq >= first_seq)
711 .take(limit)
712 .cloned(),
713 );
714 Ok(visible)
715 }
716
717 pub fn retire_visible_through(
723 &self,
724 absorbed_through: LogSeq,
725 ) -> Result<WalRetirement, WalRetireError> {
726 let capacity = {
727 let state = self.lock_state().map_err(WalRetireError::Writer)?;
728 if state
729 .durable_end
730 .is_none_or(|durable_end| absorbed_through > durable_end)
731 {
732 return Err(WalRetireError::BeyondDurable {
733 requested: absorbed_through,
734 durable_end: state.durable_end,
735 });
736 }
737 state
738 .visible
739 .iter()
740 .take_while(|record| record.seq <= absorbed_through)
741 .count()
742 };
743 let mut retired = Vec::with_capacity(capacity);
744 let retirement = {
745 let mut state = self.lock_state().map_err(WalRetireError::Writer)?;
746 if state
747 .durable_end
748 .is_none_or(|durable_end| absorbed_through > durable_end)
749 {
750 return Err(WalRetireError::BeyondDurable {
751 requested: absorbed_through,
752 durable_end: state.durable_end,
753 });
754 }
755 while state
756 .visible
757 .front()
758 .is_some_and(|record| record.seq <= absorbed_through)
759 {
760 let Some(record) = state.visible.pop_front() else {
761 break;
762 };
763 state.retained_bytes = state.retained_bytes.saturating_sub(record.encoded_len());
764 retired.push(record);
765 }
766 WalRetirement {
767 absorbed_through,
768 records_released: retired.len(),
769 encoded_bytes_released: retired.iter().fold(0_usize, |total, record| {
770 total.saturating_add(record.encoded_len())
771 }),
772 retained_records: state.visible.len(),
773 retained_bytes: state.retained_bytes,
774 durable_end: state.durable_end,
775 }
776 };
777 drop(retired);
778 Ok(retirement)
779 }
780
781 pub fn stats(&self) -> Result<WalWriterStats, WalWriteError> {
783 let mut recent_groups = Vec::with_capacity(RECENT_GROUP_LIMIT);
784 let state = self.lock_state()?;
785 recent_groups.extend(state.recent_groups.iter().copied());
786 let pending_records = state
787 .pending_groups
788 .iter()
789 .fold(0_usize, |total, group| total.saturating_add(group.records));
790 Ok(WalWriterStats {
791 completed_groups: state.completed_groups,
792 completed_records: state.completed_records,
793 completed_bytes: state.completed_bytes,
794 group_size_histogram: state.group_size_histogram,
795 recent_groups,
796 pending_records,
797 visible_end: state.visible.back().map(|record| record.seq),
798 durable_end: state.durable_end,
799 retained_records: state.visible.len(),
800 retained_bytes: state.retained_bytes,
801 encoded_buffer_allocations: state.encoded_buffer_allocations,
802 })
803 }
804
805 fn stage(&self, op: u16, payload: &[u8]) -> Result<(LogSeq, bool), WalWriteError> {
806 let encoded_len = encoded_record_len(payload.len()).map_err(WalWriteError::Record)?;
807 let mut encoded = Arc::new(Vec::with_capacity(encoded_len));
808 let mut state = self.lock_state()?;
809 loop {
810 if let Some(failure) = &state.failure {
811 return Err(WalWriteError::failed(failure));
812 }
813 Self::ensure_pending_group(&mut state);
814 let seq = LogSeq::new(state.next_seq);
815 let (required, pending_records) = state
816 .pending_groups
817 .back()
818 .map(|group| {
819 (
820 group.encoded_bytes.saturating_add(encoded_len),
821 group.records,
822 )
823 })
824 .ok_or(WalWriteError::Poisoned("pending WAL group"))?;
825 if required <= self.max_group_bytes {
826 let next_seq = state
827 .next_seq
828 .checked_add(1)
829 .ok_or(WalWriteError::SequenceExhausted)?;
830 let storage = Arc::get_mut(&mut encoded)
831 .ok_or(WalWriteError::Poisoned("unique preallocated WAL record"))?;
832 encode_record_into(WalRecord { seq, op, payload }, storage)
833 .map_err(WalWriteError::Record)?;
834 state.next_seq = next_seq;
835 state.encoded_buffer_allocations =
836 state.encoded_buffer_allocations.saturating_add(1);
837 let group = state
838 .pending_groups
839 .back_mut()
840 .ok_or(WalWriteError::Poisoned("pending WAL group"))?;
841 group.chunks.push(EncodedChunk {
842 encoded: Arc::clone(&encoded),
843 range: 0..encoded_len,
844 });
845 group.encoded_bytes = required;
846 group.records = group.records.saturating_add(1);
847 group.last_seq = Some(seq);
848 state.retained_bytes = state.retained_bytes.saturating_add(encoded_len);
849 state.visible.push_back(VisibleRecord::writer_owned(
850 seq,
851 op,
852 encoded,
853 0..encoded_len,
854 ));
855 let leader = !state.barrier_in_flight;
856 if leader {
857 state.barrier_in_flight = true;
858 }
859 return Ok((seq, leader));
860 }
861 if pending_records == 0 {
862 return Err(WalWriteError::GroupTooLarge {
863 encoded_bytes: required,
864 max_group_bytes: self.max_group_bytes,
865 });
866 }
867 state = self
868 .changed
869 .wait(state)
870 .map_err(|_| WalWriteError::Poisoned("WAL state mutex"))?;
871 }
872 }
873
874 fn flush_as_leader(&self) -> Result<(), WalWriteError> {
875 loop {
876 let (group, last_seq) = {
877 let mut state = self.lock_state()?;
878 let group = state
879 .pending_groups
880 .pop_front()
881 .ok_or(WalWriteError::Poisoned("WAL leader found no pending group"))?;
882 let last_seq = group.last_seq.ok_or(WalWriteError::Poisoned(
883 "WAL leader found no pending sequence",
884 ))?;
885 self.changed.notify_all();
886 (group, last_seq)
887 };
888 let failure = self.write_group(&group).err().map(|error| {
889 Arc::new(Failure {
890 kind: error.kind(),
891 detail: Arc::from(error.to_string()),
892 })
893 });
894 let completed = CompletedGroup {
895 records: group.records,
896 encoded_bytes: group.encoded_bytes,
897 };
898 let mut reusable_chunks = group.chunks;
899 reusable_chunks.clear();
900 let mut state = self.lock_state()?;
901 state.spare_chunks = reusable_chunks;
902 match failure {
903 None => {
904 state.durable_end = Some(last_seq);
905 self.durable_progress
906 .store(last_seq.get(), Ordering::Release);
907 state.completed_groups = state.completed_groups.saturating_add(1);
908 state.completed_records = state
909 .completed_records
910 .saturating_add(completed.records as u64);
911 state.completed_bytes = state
912 .completed_bytes
913 .saturating_add(completed.encoded_bytes as u64);
914 state.group_size_histogram.record(completed.records);
915 if state.recent_groups.len() == RECENT_GROUP_LIMIT {
916 state.recent_groups.pop_front();
917 }
918 state.recent_groups.push_back(completed);
919 self.changed.notify_all();
920 if state.pending_groups.is_empty() {
921 state.barrier_in_flight = false;
922 return Ok(());
923 }
924 }
925 Some(failure) => {
926 state.failure = Some(Arc::clone(&failure));
927 state.barrier_in_flight = false;
928 self.changed.notify_all();
929 return Err(WalWriteError::failed(&failure));
930 }
931 }
932 }
933 }
934
935 fn write_group(&self, group: &Group) -> std::io::Result<()> {
936 let mut buffers = Vec::with_capacity(group.chunks.len());
937 for chunk in &group.chunks {
938 buffers.push(IoSlice::new(chunk.bytes()?));
939 }
940 let mut file = self
941 .file
942 .lock()
943 .map_err(|_| std::io::Error::other("WAL file mutex poisoned"))?;
944 file.append_vectored(&mut buffers)?;
945 match self.sync {
946 SyncRequirement::Skip => Ok(()),
947 SyncRequirement::Sync(kind) => file.sync(kind),
948 }?;
949 drop(file);
950 let mut created_directory_sync = self
951 .created_directory_sync
952 .lock()
953 .map_err(|_| std::io::Error::other("WAL directory-sync mutex poisoned"))?;
954 if let Some(sync) = created_directory_sync.as_ref() {
955 sync.vfs.sync(&sync.directory, sync.kind)?;
956 created_directory_sync.take();
957 }
958 Ok(())
959 }
960
961 fn wait_until_durable(&self, seq: LogSeq) -> Result<(), WalWriteError> {
962 let mut state = self.lock_state()?;
963 while state
964 .durable_end
965 .is_none_or(|durable_end| durable_end < seq)
966 {
967 if let Some(failure) = &state.failure {
968 return Err(WalWriteError::failed(failure));
969 }
970 state = self
971 .changed
972 .wait(state)
973 .map_err(|_| WalWriteError::Poisoned("WAL state mutex"))?;
974 }
975 Ok(())
976 }
977
978 fn lock_state(&self) -> Result<MutexGuard<'_, WriterState>, WalWriteError> {
979 self.state
980 .lock()
981 .map_err(|_| WalWriteError::Poisoned("WAL state mutex"))
982 }
983}
984
985pub(crate) fn encode_wal_image(
986 first_seq: LogSeq,
987 records: &[(u16, Vec<u8>)],
988) -> Result<Vec<u8>, WalWriteError> {
989 let mut encoded = header::encode_header(first_seq).map_err(WalWriteError::Header)?;
990 let mut sequence = first_seq.get();
991 for (op, payload) in records {
992 record::append_record_into(
993 record::WalRecord {
994 seq: LogSeq::new(sequence),
995 op: *op,
996 payload,
997 },
998 &mut encoded,
999 )
1000 .map_err(WalWriteError::Record)?;
1001 sequence = sequence
1002 .checked_add(1)
1003 .ok_or(WalWriteError::SequenceExhausted)?;
1004 }
1005 Ok(encoded)
1006}
1007
1008fn default_max_group_bytes(policy: DurabilityPolicy) -> usize {
1009 match policy.data_file_sync() {
1010 SyncRequirement::Sync(SyncKind::Full) => DEFAULT_MAX_GROUP_BYTES_DURABLE,
1011 SyncRequirement::Skip | SyncRequirement::Sync(SyncKind::Barrier) => DEFAULT_MAX_GROUP_BYTES,
1012 }
1013}
1014
1015#[cfg(test)]
1016#[allow(clippy::expect_used)]
1017mod tests {
1018 use std::path::Path;
1019
1020 use super::{LogSeq, RecordEncodeError, WalWriteError, WalWriter, append_record_into};
1021 use crate::lifecycle::durability::{CommitTier, DurabilityMode, DurabilityPolicy};
1022 use crate::vfs::CountingVfs;
1023 use crate::vfs::crash::MemoryVfs;
1024
1025 #[test]
1026 fn durable_policy_defaults_to_a_sixteen_mib_group_cap() {
1027 let counting = CountingVfs::new(MemoryVfs::new());
1028 let policy = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::Durable)
1029 .expect("durable policy");
1030 let writer = WalWriter::create(
1031 &counting,
1032 Path::new("/wal/default-durable-cap.ze"),
1033 LogSeq::new(1),
1034 policy,
1035 )
1036 .expect("writer");
1037 let payload = vec![0x5a; 1_024];
1038 let records = (0..1_024)
1039 .map(|_| (7, payload.as_slice()))
1040 .collect::<Vec<_>>();
1041
1042 writer.commit_many(&records).expect("large durable batch");
1043 let stats = writer.stats().expect("writer stats");
1044
1045 assert_eq!(
1046 (
1047 counting.append_calls(),
1048 counting.handle_full_sync_calls(),
1049 stats.completed_groups,
1050 stats.completed_records,
1051 ),
1052 (1, 1, 1, 1_024),
1053 "a 1,071,144-byte durable batch must share one append and full sync"
1054 );
1055 }
1056
1057 #[test]
1058 fn ordered_and_skip_policies_keep_the_one_mib_default() {
1059 let payload = vec![0x6b; 1_024];
1060 let records = (0..1_024)
1061 .map(|_| (8, payload.as_slice()))
1062 .collect::<Vec<_>>();
1063 let mut actual = Vec::new();
1064
1065 for (tier, path) in [
1066 (CommitTier::Ordered, "/wal/default-ordered-cap.ze"),
1067 (CommitTier::None, "/wal/default-skip-cap.ze"),
1068 ] {
1069 let counting = CountingVfs::new(MemoryVfs::new());
1070 let policy =
1071 DurabilityPolicy::new(DurabilityMode::Durable, tier).expect("supported policy");
1072 let writer = WalWriter::create(&counting, Path::new(path), LogSeq::new(1), policy)
1073 .expect("writer");
1074
1075 writer.commit_many(&records).expect("large batch");
1076 let stats = writer.stats().expect("writer stats");
1077 actual.push((
1078 tier,
1079 counting.append_calls(),
1080 counting.handle_barrier_sync_calls(),
1081 counting.handle_full_sync_calls(),
1082 stats.completed_groups,
1083 ));
1084 }
1085
1086 assert_eq!(
1087 actual,
1088 vec![
1089 (CommitTier::Ordered, 2, 2, 0, 2),
1090 (CommitTier::None, 2, 0, 0, 2),
1091 ],
1092 "ordered and skip policies must split a 1,071,144-byte batch at one MiB"
1093 );
1094 }
1095
1096 #[test]
1097 fn an_explicit_group_cap_still_wins() {
1098 let counting = CountingVfs::new(MemoryVfs::new());
1099 let policy = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::Durable)
1100 .expect("durable policy");
1101 let writer = WalWriter::create_with_max_group_bytes(
1102 &counting,
1103 Path::new("/wal/explicit-group-cap.ze"),
1104 LogSeq::new(1),
1105 policy,
1106 128,
1107 )
1108 .expect("writer");
1109 writer.commit(0, &[0; 10]).expect("flush WAL header");
1110 let groups_before = writer.stats().expect("prime stats").recent_groups.len();
1111 counting.reset();
1112 let payload = [0x7c; 10];
1113 let records = (0..12).map(|_| (9, payload.as_slice())).collect::<Vec<_>>();
1114
1115 writer.commit_many(&records).expect("explicit-cap batch");
1116 let stats = writer.stats().expect("batch stats");
1117 let groups = stats
1118 .recent_groups
1119 .iter()
1120 .skip(groups_before)
1121 .map(|group| (group.records, group.encoded_bytes))
1122 .collect::<Vec<_>>();
1123
1124 assert_eq!(
1125 (
1126 counting.append_calls(),
1127 counting.handle_full_sync_calls(),
1128 groups,
1129 ),
1130 (3, 3, vec![(4, 128), (4, 128), (4, 128)]),
1131 "an explicit 128-byte cap must override the durable default"
1132 );
1133 }
1134
1135 #[test]
1136 fn encoding_failure_in_middle_of_commit_many_is_atomic() {
1137 let policy = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::None)
1138 .expect("supported policy");
1139 let writer = WalWriter::create(
1140 &MemoryVfs::new(),
1141 Path::new("/wal/unit-encode-failure.ze"),
1142 LogSeq::new(1),
1143 policy,
1144 )
1145 .expect("writer");
1146 let records = [(51, b"first".as_slice()), (52, b"second".as_slice())];
1147 let mut encode_calls = 0_usize;
1148
1149 let result = writer.stage_many_with_encoder(&records, |record, storage| {
1150 encode_calls = encode_calls.saturating_add(1);
1151 if encode_calls == 2 {
1152 Err(RecordEncodeError::PayloadTooLarge(usize::MAX))
1153 } else {
1154 append_record_into(record, storage)
1155 }
1156 });
1157 let failure = match result {
1158 Err(WalWriteError::Record(error)) => format!("error={error}"),
1159 other => format!("unexpected={other:?}"),
1160 };
1161 let visible = writer
1162 .visible_records(LogSeq::new(1), records.len())
1163 .expect("visibility after failed encoding")
1164 .len();
1165 let next = writer
1166 .commit(53, b"after")
1167 .expect("sequence remains reusable");
1168
1169 assert_eq!(
1170 (failure, visible, next),
1171 (
1172 format!("error=WAL payload length {} exceeds u32", usize::MAX),
1173 0,
1174 LogSeq::new(1),
1175 ),
1176 "a failure encoding any batch record must consume no sequence and publish no visibility"
1177 );
1178 }
1179}
1180
1181impl DurableLog for WalWriter {
1182 fn durable_end(&self) -> u64 {
1183 self.durable_progress.load(Ordering::Acquire)
1184 }
1185}
1186
1187#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1189pub enum VisibleRecordError {
1190 InvalidEncodedRange {
1192 start: usize,
1194 end: usize,
1196 available: usize,
1198 },
1199 Record(RecordError),
1201}
1202
1203impl std::fmt::Display for VisibleRecordError {
1204 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1205 match self {
1206 Self::InvalidEncodedRange {
1207 start,
1208 end,
1209 available,
1210 } => write!(
1211 formatter,
1212 "visible WAL encoded range {start}..{end} exceeds {available} bytes"
1213 ),
1214 Self::Record(error) => write!(formatter, "visible WAL record: {error}"),
1215 }
1216 }
1217}
1218
1219impl std::error::Error for VisibleRecordError {
1220 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1221 match self {
1222 Self::InvalidEncodedRange { .. } => None,
1223 Self::Record(error) => Some(error),
1224 }
1225 }
1226}
1227
1228#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1230pub struct WalRetirement {
1231 pub absorbed_through: LogSeq,
1233 pub records_released: usize,
1235 pub encoded_bytes_released: usize,
1237 pub retained_records: usize,
1239 pub retained_bytes: usize,
1241 pub durable_end: Option<LogSeq>,
1243}
1244
1245#[derive(Debug)]
1247pub enum WalRetireError {
1248 BeyondDurable {
1250 requested: LogSeq,
1252 durable_end: Option<LogSeq>,
1254 },
1255 Writer(WalWriteError),
1257}
1258
1259impl std::fmt::Display for WalRetireError {
1260 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1261 match self {
1262 Self::BeyondDurable {
1263 requested,
1264 durable_end: Some(durable_end),
1265 } => write!(
1266 formatter,
1267 "cannot retire WAL sequence {} past durable end {}",
1268 requested.get(),
1269 durable_end.get()
1270 ),
1271 Self::BeyondDurable {
1272 requested,
1273 durable_end: None,
1274 } => write!(
1275 formatter,
1276 "cannot retire WAL sequence {} before any sequence is durable",
1277 requested.get()
1278 ),
1279 Self::Writer(error) => write!(formatter, "cannot retire visible WAL records: {error}"),
1280 }
1281 }
1282}
1283
1284impl std::error::Error for WalRetireError {
1285 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1286 match self {
1287 Self::BeyondDurable { .. } => None,
1288 Self::Writer(error) => Some(error),
1289 }
1290 }
1291}
1292
1293#[derive(Clone, Debug, Eq, PartialEq)]
1295pub struct WalReader {
1296 records: Vec<VisibleRecord>,
1297 next_seq: u64,
1298 durable_end: u64,
1299 terminator: Option<ReplayTerminator>,
1300}
1301
1302pub(crate) struct CleanWalReader {
1303 records: Vec<VisibleRecord>,
1304 next_seq: u64,
1305 durable_end: Option<LogSeq>,
1306}
1307
1308impl CleanWalReader {
1309 pub(crate) fn records(&self) -> &[VisibleRecord] {
1310 &self.records
1311 }
1312}
1313
1314impl WalReader {
1315 pub fn open(vfs: &dyn Vfs, path: &Path) -> Result<Self, WalReadError> {
1317 let bytes = match vfs.read(path) {
1318 Ok(bytes) => bytes,
1319 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1320 return Ok(Self {
1321 records: Vec::new(),
1322 next_seq: 0,
1323 durable_end: 0,
1324 terminator: None,
1325 });
1326 }
1327 Err(error) => return Err(WalReadError::Io(error)),
1328 };
1329 let replayed = replay(&bytes);
1330 let terminator = replayed.terminator;
1331 if matches!(terminator, ReplayTerminator::InvalidHeader(_)) {
1332 return Ok(Self {
1333 records: Vec::new(),
1334 next_seq: 0,
1335 durable_end: 0,
1336 terminator: Some(terminator),
1337 });
1338 }
1339 let header = header::decode_header(&bytes).map_err(WalReadError::Header)?;
1340 let mut offset = header::WAL_HEADER_LEN;
1341 let recovered = replayed
1342 .records
1343 .iter()
1344 .map(|record| {
1345 let encoded_len = record::MIN_RECORD_LEN.saturating_add(record.payload.len());
1346 let range = offset..offset.saturating_add(encoded_len);
1347 offset = range.end;
1348 (record.seq, record.op, range)
1349 })
1350 .collect::<Vec<_>>();
1351 drop(replayed);
1352 let records = recovered
1353 .into_iter()
1354 .map(|(seq, op, range)| {
1355 let start = range.start;
1356 let end = range.end;
1357 let encoded = bytes
1358 .get(range)
1359 .ok_or(WalReadError::InvalidRecoveredRange {
1360 start,
1361 end,
1362 available: bytes.len(),
1363 })?;
1364 let encoded = Arc::new(encoded.to_vec());
1365 let length = encoded.len();
1366 Ok(VisibleRecord::recovered(seq, op, encoded, 0..length))
1367 })
1368 .collect::<Result<Vec<_>, WalReadError>>()?;
1369 let durable_end = records.last().map_or_else(
1370 || header.first_seq.get().saturating_sub(1),
1371 |record| record.seq.get(),
1372 );
1373 let next_seq = records.last().map_or(header.first_seq.get(), |record| {
1374 record.seq.get().saturating_add(1)
1375 });
1376 Ok(Self {
1377 records,
1378 next_seq,
1379 durable_end,
1380 terminator: Some(terminator),
1381 })
1382 }
1383
1384 #[must_use]
1386 pub fn records(&self) -> &[VisibleRecord] {
1387 &self.records
1388 }
1389
1390 #[must_use]
1392 pub const fn terminator(&self) -> Option<ReplayTerminator> {
1393 self.terminator
1394 }
1395
1396 pub(crate) fn into_clean(self) -> Result<CleanWalReader, WalRecoveryError> {
1397 match self.terminator {
1398 Some(ReplayTerminator::CleanEnd) => {
1399 let durable_end = Some(LogSeq::new(self.durable_end));
1400 Ok(CleanWalReader {
1401 records: self.records,
1402 next_seq: self.next_seq,
1403 durable_end,
1404 })
1405 }
1406 Some(ReplayTerminator::InvalidHeader(error)) => {
1407 let error = WalRecoveryError::InvalidHeader(error);
1408 #[cfg(any(test, feature = "test-seams"))]
1409 crate::lifecycle::record_storage_wal_recovery_fault(&error);
1410 Err(error)
1411 }
1412 Some(ReplayTerminator::CorruptAt { offset, reason }) => {
1413 let error = WalRecoveryError::CorruptAt { offset, reason };
1414 #[cfg(any(test, feature = "test-seams"))]
1415 crate::lifecycle::record_storage_wal_recovery_fault(&error);
1416 Err(error)
1417 }
1418 None => Err(WalRecoveryError::MissingFile),
1419 }
1420 }
1421}
1422
1423impl DurableLog for WalReader {
1424 fn durable_end(&self) -> u64 {
1425 self.durable_end
1426 }
1427}
1428
1429#[derive(Debug)]
1431pub enum WalWriteError {
1432 NonEmptyWal {
1434 length: u64,
1436 },
1437 RecoveredBytesOverflow,
1439 GroupTooLarge {
1441 encoded_bytes: usize,
1443 max_group_bytes: usize,
1445 },
1446 SequenceExhausted,
1448 Header(RegistryError),
1450 Record(RecordEncodeError),
1452 Poisoned(&'static str),
1454 Failed {
1456 kind: std::io::ErrorKind,
1458 detail: Arc<str>,
1460 },
1461}
1462
1463impl WalWriteError {
1464 fn from_io(error: std::io::Error) -> Self {
1465 Self::Failed {
1466 kind: error.kind(),
1467 detail: Arc::from(error.to_string()),
1468 }
1469 }
1470
1471 fn failed(failure: &Failure) -> Self {
1472 Self::Failed {
1473 kind: failure.kind,
1474 detail: Arc::clone(&failure.detail),
1475 }
1476 }
1477}
1478
1479impl std::fmt::Display for WalWriteError {
1480 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1481 match self {
1482 Self::NonEmptyWal { length } => {
1483 write!(formatter, "WAL path is not empty: {length} bytes")
1484 }
1485 Self::RecoveredBytesOverflow => {
1486 formatter.write_str("recovered WAL record bytes overflow usize")
1487 }
1488 Self::GroupTooLarge {
1489 encoded_bytes,
1490 max_group_bytes,
1491 } => write!(
1492 formatter,
1493 "WAL group needs {encoded_bytes} bytes, exceeding {max_group_bytes}"
1494 ),
1495 Self::SequenceExhausted => formatter.write_str("WAL sequence space exhausted"),
1496 Self::Header(error) => write!(formatter, "WAL header: {error}"),
1497 Self::Record(error) => write!(formatter, "WAL record: {error}"),
1498 Self::Poisoned(detail) => write!(formatter, "{detail} poisoned"),
1499 Self::Failed { kind, detail } => {
1500 write!(formatter, "WAL writer failed with {:?}: {}", kind, detail)
1501 }
1502 }
1503 }
1504}
1505
1506impl std::error::Error for WalWriteError {}
1507
1508#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1510pub enum WalRecoveryError {
1511 MissingFile,
1513 InvalidHeader(header::WalHeaderError),
1515 CorruptAt {
1517 offset: usize,
1519 reason: replay::CorruptionReason,
1521 },
1522}
1523
1524impl std::fmt::Display for WalRecoveryError {
1525 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1526 match self {
1527 Self::MissingFile => formatter.write_str("WAL recovery path is missing"),
1528 Self::InvalidHeader(error) => write!(formatter, "WAL recovery header: {error}"),
1529 Self::CorruptAt { offset, reason } => {
1530 write!(
1531 formatter,
1532 "WAL recovery stopped at byte {offset}: {reason:?}"
1533 )
1534 }
1535 }
1536 }
1537}
1538
1539impl std::error::Error for WalRecoveryError {
1540 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1541 match self {
1542 Self::InvalidHeader(error) => Some(error),
1543 Self::MissingFile | Self::CorruptAt { .. } => None,
1544 }
1545 }
1546}
1547
1548#[derive(Debug)]
1550pub enum WalReadError {
1551 Io(std::io::Error),
1553 Header(header::WalHeaderError),
1555 InvalidRecoveredRange {
1557 start: usize,
1559 end: usize,
1561 available: usize,
1563 },
1564}
1565
1566impl std::fmt::Display for WalReadError {
1567 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1568 match self {
1569 Self::Io(error) => write!(formatter, "WAL read: {error}"),
1570 Self::Header(error) => write!(formatter, "WAL read header: {error}"),
1571 Self::InvalidRecoveredRange {
1572 start,
1573 end,
1574 available,
1575 } => write!(
1576 formatter,
1577 "WAL recovered range {start}..{end} exceeds {available} bytes"
1578 ),
1579 }
1580 }
1581}
1582
1583impl std::error::Error for WalReadError {
1584 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1585 match self {
1586 Self::Io(error) => Some(error),
1587 Self::Header(error) => Some(error),
1588 Self::InvalidRecoveredRange { .. } => None,
1589 }
1590 }
1591}