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