1use std::path::Path;
26use std::sync::atomic::{AtomicU64, Ordering};
27
28#[cfg(target_os = "linux")]
29use std::os::fd::AsRawFd;
30
31use memmap2::Mmap;
32
33use crate::crypto::KeyRing;
34use crate::error::{Result, WalError};
35use crate::preamble::{PREAMBLE_SIZE, SegmentPreamble, WAL_PREAMBLE_MAGIC, parse_leading_preamble};
36use crate::reader::StopReason;
37use crate::record::{
38 HEADER_SIZE, RecordHeader, RecordType, WAL_MAGIC, WalRecord, is_format_gap, may_be_format_gap,
39};
40use crate::segment::SegmentDecryptor;
41
42fn checked_range_end(start: usize, len: usize, limit: usize) -> Result<Option<usize>> {
43 let end = start.checked_add(len).ok_or_else(|| {
44 WalError::Io(std::io::Error::new(
45 std::io::ErrorKind::InvalidData,
46 "mmap WAL offset overflow",
47 ))
48 })?;
49 Ok((end <= limit).then_some(end))
50}
51
52pub mod observability {
56 use super::{AtomicU64, Ordering};
57 pub(super) static SEGMENTS_OPENED: AtomicU64 = AtomicU64::new(0);
58 pub(super) static FADV_DONTNEED_COUNT: AtomicU64 = AtomicU64::new(0);
59 pub(super) static MADV_SEQUENTIAL_COUNT: AtomicU64 = AtomicU64::new(0);
60
61 pub fn segments_opened() -> u64 {
62 SEGMENTS_OPENED.load(Ordering::Relaxed)
63 }
64 pub fn fadv_dontneed_count() -> u64 {
65 FADV_DONTNEED_COUNT.load(Ordering::Relaxed)
66 }
67 pub fn madv_sequential_count() -> u64 {
68 MADV_SEQUENTIAL_COUNT.load(Ordering::Relaxed)
69 }
70}
71
72fn fadv_dontneed(fd: &std::fs::File, len: usize, path: &Path) {
78 if len == 0 {
79 return;
80 }
81 #[cfg(target_os = "linux")]
82 {
83 let len = match libc::off_t::try_from(len) {
84 Ok(len) => len,
85 Err(_) => {
86 tracing::warn!(
87 path = %path.display(),
88 "WAL segment length does not fit posix_fadvise offset; skipping cache release",
89 );
90 return;
91 }
92 };
93 let rc = unsafe { libc::posix_fadvise(fd.as_raw_fd(), 0, len, libc::POSIX_FADV_DONTNEED) };
94 if rc == 0 {
95 observability::FADV_DONTNEED_COUNT.fetch_add(1, Ordering::Relaxed);
96 } else {
97 tracing::warn!(
98 path = %path.display(),
99 errno = rc,
100 "posix_fadvise(DONTNEED) failed on exhausted WAL segment",
101 );
102 }
103 }
104 #[cfg(not(target_os = "linux"))]
105 {
106 let _ = (fd, path);
107 }
108}
109
110pub struct MmapWalReader {
116 mmap: Mmap,
117 offset: usize,
118 records_start: usize,
122 segment_preamble: Option<SegmentPreamble>,
125 file: std::fs::File,
126 path: std::path::PathBuf,
127 madvise_state: Option<libc::c_int>,
128 stop_reason: Option<StopReason>,
129 decryptor: Option<SegmentDecryptor>,
132}
133
134impl MmapWalReader {
135 pub fn open(path: &Path, keys: Option<&KeyRing>) -> Result<Self> {
144 let mut reader = Self::open_raw(path)?;
145 reader.decryptor = Some(SegmentDecryptor::new(
146 reader.segment_preamble.as_ref(),
147 keys,
148 ));
149 Ok(reader)
150 }
151
152 pub fn open_raw(path: &Path) -> Result<Self> {
162 observability::SEGMENTS_OPENED.fetch_add(1, Ordering::Relaxed);
163 let file = std::fs::File::open(path)?;
164 let mmap = unsafe { Mmap::map(&file)? };
168
169 let mut madvise_state = None;
173 if !mmap.is_empty() {
174 let rc = unsafe {
175 libc::madvise(
176 mmap.as_ptr() as *mut libc::c_void,
177 mmap.len(),
178 libc::MADV_SEQUENTIAL,
179 )
180 };
181 if rc == 0 {
182 madvise_state = Some(libc::MADV_SEQUENTIAL);
183 observability::MADV_SEQUENTIAL_COUNT.fetch_add(1, Ordering::Relaxed);
184 } else {
185 tracing::warn!(
186 path = %path.display(),
187 errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
188 "madvise(MADV_SEQUENTIAL) failed on WAL segment; continuing",
189 );
190 }
191 }
192
193 let segment_preamble = parse_leading_preamble(&mmap, &WAL_PREAMBLE_MAGIC)?;
194 let offset = if segment_preamble.is_some() {
195 PREAMBLE_SIZE
196 } else {
197 0
198 };
199
200 Ok(Self {
201 mmap,
202 offset,
203 records_start: offset,
204 segment_preamble,
205 file,
206 path: path.to_path_buf(),
207 madvise_state,
208 stop_reason: None,
209 decryptor: None,
210 })
211 }
212
213 fn deliver(&self, record: WalRecord) -> Result<Option<WalRecord>> {
216 match &self.decryptor {
217 Some(decryptor) => decryptor.decrypt_record(record).map(Some),
218 None => Ok(Some(record)),
219 }
220 }
221
222 pub fn segment_preamble(&self) -> Option<&SegmentPreamble> {
226 self.segment_preamble.as_ref()
227 }
228
229 pub fn stop_reason(&self) -> Option<StopReason> {
234 self.stop_reason
235 }
236
237 fn stop(&mut self, reason: StopReason) -> Result<Option<WalRecord>> {
238 self.stop_reason = Some(reason);
239 Ok(None)
240 }
241
242 pub fn madvise_state(&self) -> Option<libc::c_int> {
244 self.madvise_state
245 }
246
247 pub fn release_pages(&self) {
250 fadv_dontneed(&self.file, self.mmap.len(), &self.path);
251 }
252
253 pub fn next_record(&mut self) -> Result<Option<WalRecord>> {
264 let data = &self.mmap[..];
265
266 loop {
267 let record_offset = self.offset;
270 let Some(header_end) = checked_range_end(self.offset, HEADER_SIZE, data.len())? else {
271 let reason = if record_offset == data.len() {
272 StopReason::Eof
273 } else {
274 StopReason::Corruption {
275 offset: record_offset as u64,
276 }
277 };
278 return self.stop(reason);
279 };
280
281 let header_bytes: &[u8; HEADER_SIZE] = data
283 .get(self.offset..header_end)
284 .and_then(|bytes| bytes.try_into().ok())
285 .ok_or_else(|| {
286 WalError::Io(std::io::Error::new(
287 std::io::ErrorKind::InvalidData,
288 "header slice conversion failed",
289 ))
290 })?;
291 let header = RecordHeader::from_bytes(header_bytes);
292
293 if header.magic != WAL_MAGIC {
295 return self.stop(StopReason::Corruption {
296 offset: record_offset as u64,
297 });
298 }
299
300 let header_offset = u64::try_from(self.offset).map_err(|_| {
303 WalError::Io(std::io::Error::new(
304 std::io::ErrorKind::InvalidData,
305 "mmap WAL offset does not fit u64",
306 ))
307 })?;
308 match header.validate(header_offset) {
309 Ok(()) => {}
310 Err(error @ WalError::PayloadTooLarge { .. }) => return Err(error),
311 Err(error @ WalError::UnsupportedVersion { .. })
315 if may_be_format_gap(&header, self.offset == self.records_start) =>
316 {
317 let payload = usize::try_from(header.payload_len)
318 .ok()
319 .filter(|len| *len <= crate::record::MAX_WAL_PAYLOAD_SIZE)
320 .and_then(|len| data.get(header_end..header_end.checked_add(len)?));
321 if is_format_gap(&header, payload) {
322 return Err(error);
323 }
324 return self.stop(StopReason::Corruption {
325 offset: header_offset,
326 });
327 }
328 Err(_) => {
329 return self.stop(StopReason::Corruption {
330 offset: header_offset,
331 });
332 }
333 }
334
335 let payload_len =
336 usize::try_from(header.payload_len).map_err(|_| WalError::PayloadTooLarge {
337 size: usize::MAX,
338 max: crate::record::MAX_WAL_PAYLOAD_SIZE,
339 })?;
340 let Some(record_end) = checked_range_end(header_end, payload_len, data.len())? else {
341 return self.stop(StopReason::Corruption {
343 offset: header_offset,
344 });
345 };
346
347 let payload = data
349 .get(header_end..record_end)
350 .ok_or_else(|| {
351 WalError::Io(std::io::Error::new(
352 std::io::ErrorKind::InvalidData,
353 "payload slice conversion failed",
354 ))
355 })?
356 .to_vec();
357 self.offset = record_end;
358
359 let record = WalRecord { header, payload };
360
361 if record.verify_checksum().is_err() {
363 return self.stop(StopReason::Corruption {
364 offset: header_offset,
365 });
366 }
367
368 let logical_type = record.logical_record_type();
370 let Some(kind) = RecordType::from_raw(logical_type) else {
371 if RecordType::is_required(logical_type) {
372 return Err(WalError::UnknownRequiredRecordType {
373 record_type: header.record_type,
374 lsn: header.lsn,
375 });
376 }
377 continue;
379 };
380
381 if kind == RecordType::Noop {
383 continue;
384 }
385
386 return self.deliver(record);
387 }
388 }
389
390 pub fn records(self) -> MmapRecordIter {
392 MmapRecordIter { reader: self }
393 }
394
395 pub fn offset(&self) -> usize {
397 self.offset
398 }
399
400 pub fn len(&self) -> usize {
402 self.mmap.len()
403 }
404
405 pub fn is_empty(&self) -> bool {
407 self.mmap.is_empty()
408 }
409}
410
411pub struct MmapRecordIter {
413 reader: MmapWalReader,
414}
415
416impl Iterator for MmapRecordIter {
417 type Item = Result<WalRecord>;
418
419 fn next(&mut self) -> Option<Self::Item> {
420 match self.reader.next_record() {
421 Ok(Some(record)) => Some(Ok(record)),
422 Ok(None) => None,
423 Err(e) => Some(Err(e)),
424 }
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::record::RecordType;
432 use crate::writer::{WalWriter, WalWriterConfig};
433
434 fn test_writer(path: &Path) -> WalWriter {
435 let config = WalWriterConfig {
436 use_direct_io: false, ..Default::default()
438 };
439 WalWriter::open(path, config).unwrap()
440 }
441
442 #[test]
443 fn mmap_reader_basic() {
444 let dir = tempfile::tempdir().unwrap();
445 let path = dir.path().join("test.wal");
446
447 {
449 let mut writer = test_writer(&path);
450 writer
451 .append(RecordType::Put as u32, 1, 0, 0, b"hello")
452 .unwrap();
453 writer
454 .append(RecordType::Put as u32, 1, 0, 0, b"world")
455 .unwrap();
456 writer.sync().unwrap();
457 }
458
459 let reader = MmapWalReader::open(&path, None).unwrap();
461 assert!(
462 reader.segment_preamble().is_none(),
463 "unencrypted segments carry no preamble"
464 );
465 let records: Vec<WalRecord> = reader.records().collect::<Result<Vec<_>>>().unwrap();
466
467 assert_eq!(records.len(), 2);
468 assert_eq!(records[0].payload, b"hello");
469 assert_eq!(records[1].payload, b"world");
470 }
471
472 #[test]
473 fn mmap_reader_empty_file() {
474 let dir = tempfile::tempdir().unwrap();
475 let path = dir.path().join("empty.wal");
476 std::fs::write(&path, []).unwrap();
477
478 let reader = MmapWalReader::open(&path, None).unwrap();
479 let records: Vec<WalRecord> = reader.records().collect::<Result<Vec<_>>>().unwrap();
480 assert!(records.is_empty());
481 }
482
483 #[test]
484 fn mmap_reader_truncated_header() {
485 let dir = tempfile::tempdir().unwrap();
486 let path = dir.path().join("truncated.wal");
487 std::fs::write(&path, [0u8; 10]).unwrap();
489
490 let reader = MmapWalReader::open(&path, None).unwrap();
491 let records: Vec<WalRecord> = reader.records().collect::<Result<Vec<_>>>().unwrap();
492 assert!(records.is_empty());
493 }
494
495 #[test]
496 fn mmap_reader_truncated_payload_stops_at_committed_prefix() {
497 let dir = tempfile::tempdir().unwrap();
498 let path = dir.path().join("truncated-payload.wal");
499 let header = RecordHeader {
500 magic: WAL_MAGIC,
501 format_version: crate::record::WAL_FORMAT_VERSION,
502 record_type: RecordType::Put as u32,
503 lsn: 1,
504 tenant_id: 1,
505 vshard_id: 0,
506 payload_len: 1,
507 database_id: 0,
508 apply_key: 0,
509 event_source: crate::record::NO_EVENT_SOURCE,
510 commit_hlc: 0,
511 crc32c: 0,
512 };
513 std::fs::write(&path, header.to_bytes()).unwrap();
514
515 let mut reader = MmapWalReader::open(&path, None).unwrap();
516 assert!(reader.next_record().unwrap().is_none());
517 }
518
519 #[test]
520 fn oversized_payload_header_is_a_typed_error_before_allocation() {
521 let dir = tempfile::tempdir().unwrap();
522 let path = dir.path().join("oversized.wal");
523 let header = RecordHeader {
524 magic: WAL_MAGIC,
525 format_version: crate::record::WAL_FORMAT_VERSION,
526 record_type: RecordType::Put as u32,
527 lsn: 1,
528 tenant_id: 1,
529 vshard_id: 0,
530 payload_len: (crate::record::MAX_WAL_PAYLOAD_SIZE + 1) as u32,
531 database_id: 0,
532 apply_key: 0,
533 event_source: crate::record::NO_EVENT_SOURCE,
534 commit_hlc: 0,
535 crc32c: 0,
536 };
537 std::fs::write(&path, header.to_bytes()).unwrap();
538
539 let mut reader = MmapWalReader::open(&path, None).unwrap();
540 assert!(matches!(
541 reader.next_record(),
542 Err(WalError::PayloadTooLarge { .. })
543 ));
544 }
545
546 #[test]
549 fn an_unknown_format_version_at_the_first_record_is_an_error() {
550 let dir = tempfile::tempdir().unwrap();
551 let path = dir.path().join("unknown.wal");
552 {
553 let mut writer = test_writer(&path);
554 writer
555 .append(RecordType::Put as u32, 1, 0, 0, b"x")
556 .unwrap();
557 writer.sync().unwrap();
558 }
559
560 let mut bytes = std::fs::read(&path).unwrap();
561 crate::record::foreign_version::reseal_with_version(
562 &mut bytes,
563 0,
564 crate::record::WAL_FORMAT_VERSION + 1,
565 );
566 std::fs::write(&path, &bytes).unwrap();
567
568 let mut reader = MmapWalReader::open(&path, None).unwrap();
569 match reader.next_record() {
570 Err(WalError::UnsupportedVersion { version, supported }) => {
571 assert_eq!(version, crate::record::WAL_FORMAT_VERSION + 1);
572 assert_eq!(supported, crate::record::WAL_FORMAT_VERSION);
573 }
574 other => panic!("expected UnsupportedVersion, got {other:?}"),
575 }
576 }
577
578 #[test]
581 fn a_version_mismatch_after_the_first_record_is_damage() {
582 let dir = tempfile::tempdir().unwrap();
583 let path = dir.path().join("two-records.wal");
584 {
585 let mut writer = test_writer(&path);
586 writer
587 .append(RecordType::Put as u32, 1, 0, 0, b"first")
588 .unwrap();
589 writer
590 .append(RecordType::Put as u32, 1, 0, 0, b"second")
591 .unwrap();
592 writer.sync().unwrap();
593 }
594
595 let second_at = {
596 let mut reader = MmapWalReader::open(&path, None).unwrap();
597 reader
598 .next_record()
599 .unwrap()
600 .expect("the first record reads");
601 reader.offset()
602 };
603
604 let mut bytes = std::fs::read(&path).unwrap();
605 let version_at = second_at + 4;
606 bytes[version_at..version_at + 2]
607 .copy_from_slice(&(crate::record::WAL_FORMAT_VERSION + 1).to_le_bytes());
608 std::fs::write(&path, &bytes).unwrap();
609
610 let mut reader = MmapWalReader::open(&path, None).unwrap();
611 let first = reader
612 .next_record()
613 .unwrap()
614 .expect("the first record is still readable");
615 assert_eq!(first.header.lsn, 1);
616 assert!(
617 reader.next_record().unwrap().is_none(),
618 "the mismatch stops the stream instead of failing the read"
619 );
620 let second_at = u64::try_from(second_at).unwrap();
621 assert_eq!(
622 reader.stop_reason(),
623 Some(StopReason::Corruption { offset: second_at }),
624 "a mismatch after the first record is damage, not a format gap"
625 );
626 }
627
628 #[test]
629 fn checked_range_end_rejects_overflow_and_short_ranges() {
630 assert!(checked_range_end(usize::MAX, 1, usize::MAX).is_err());
631 assert_eq!(checked_range_end(5, 2, 6).unwrap(), None);
632 }
633}