1use std::fs::File;
35use std::io::{Read, Seek, SeekFrom};
36use std::path::Path;
37
38use crate::crypto::KeyRing;
39use crate::error::{Result, WalError};
40use crate::preamble::{SegmentPreamble, WAL_PREAMBLE_MAGIC, read_leading_preamble};
41use crate::reader::StopReason;
42use crate::record::foreign_version::read_declared_payload;
43use crate::record::{
44 HEADER_SIZE, MAX_WAL_PAYLOAD_SIZE, RecordHeader, RecordType, WalRecord, is_format_gap,
45 may_be_format_gap,
46};
47use crate::segment::SegmentDecryptor;
48
49fn checked_offset_add(offset: u64, len: u64) -> Result<u64> {
50 offset.checked_add(len).ok_or_else(|| {
51 WalError::Io(std::io::Error::new(
52 std::io::ErrorKind::InvalidData,
53 "WAL lazy-reader offset overflow",
54 ))
55 })
56}
57
58pub struct LazyWalReader {
60 file: File,
61 offset: u64,
62 records_start: u64,
66 segment_preamble: Option<SegmentPreamble>,
69 double_write: Option<crate::double_write::DoubleWriteBuffer>,
70 stop_reason: Option<StopReason>,
71 decryptor: SegmentDecryptor,
75}
76
77impl LazyWalReader {
78 pub fn open(path: &Path, keys: Option<&KeyRing>) -> Result<Self> {
87 let mut file = File::open(path)?;
88 let dwb_path = path.with_extension("dwb");
89 let double_write = if dwb_path.exists() {
90 crate::double_write::DoubleWriteBuffer::open(
91 &dwb_path,
92 crate::double_write::DwbMode::Buffered,
93 )
94 .ok()
95 } else {
96 None
97 };
98 let (segment_preamble, start_offset) =
99 read_leading_preamble(&mut file, &WAL_PREAMBLE_MAGIC)?;
100 let decryptor = SegmentDecryptor::new(segment_preamble.as_ref(), keys);
101
102 Ok(Self {
103 file,
104 offset: start_offset,
105 records_start: start_offset,
106 segment_preamble,
107 double_write,
108 stop_reason: None,
109 decryptor,
110 })
111 }
112
113 pub fn segment_preamble(&self) -> Option<&SegmentPreamble> {
117 self.segment_preamble.as_ref()
118 }
119
120 pub fn stop_reason(&self) -> Option<StopReason> {
125 self.stop_reason
126 }
127
128 fn stop(&mut self, reason: StopReason) -> Result<Option<RecordHeader>> {
129 self.stop_reason = Some(reason);
130 Ok(None)
131 }
132
133 pub fn next_header(&mut self) -> Result<Option<RecordHeader>> {
143 loop {
144 let record_offset = self.offset;
145 let mut header_buf = [0u8; HEADER_SIZE];
146 match self.read_exact(&mut header_buf) {
147 Ok(()) => {}
148 Err(WalError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
149 let reason = if record_offset == self.file.metadata()?.len() {
150 StopReason::Eof
151 } else {
152 StopReason::Corruption {
153 offset: record_offset,
154 }
155 };
156 return self.stop(reason);
157 }
158 Err(e) => return Err(e),
159 }
160
161 let header = RecordHeader::from_bytes(&header_buf);
162
163 match header.validate(record_offset) {
164 Ok(()) => {}
165 Err(error @ WalError::PayloadTooLarge { .. }) => return Err(error),
166 Err(error @ WalError::UnsupportedVersion { .. })
170 if may_be_format_gap(&header, record_offset == self.records_start) =>
171 {
172 let payload = read_declared_payload(&mut self.file, &header, self.offset)?;
173 if is_format_gap(&header, payload.as_deref()) {
174 return Err(error);
175 }
176 return self.stop(StopReason::Corruption {
177 offset: record_offset,
178 });
179 }
180 Err(_) => {
181 return self.stop(StopReason::Corruption {
182 offset: record_offset,
183 });
184 }
185 }
186
187 let logical_type = header.logical_record_type();
189 match RecordType::from_raw(logical_type) {
190 Some(RecordType::Noop) => {
193 self.skip_payload(&header)?;
194 continue;
195 }
196 Some(_) => {}
197 None if RecordType::is_required(logical_type) => {
198 return Err(WalError::UnknownRequiredRecordType {
199 record_type: header.record_type,
200 lsn: header.lsn,
201 });
202 }
203 None => {}
204 }
205
206 return Ok(Some(header));
207 }
208 }
209
210 pub fn read_payload(&mut self, header: &RecordHeader) -> Result<Vec<u8>> {
217 let raw = self.read_raw_payload(header)?;
218 self.decryptor.decrypt_payload(header, raw)
219 }
220
221 fn read_raw_payload(&mut self, header: &RecordHeader) -> Result<Vec<u8>> {
226 header.validate(self.offset)?;
227 let payload_len =
228 usize::try_from(header.payload_len).map_err(|_| WalError::PayloadTooLarge {
229 size: usize::MAX,
230 max: MAX_WAL_PAYLOAD_SIZE,
231 })?;
232 let mut payload = vec![0u8; payload_len];
233 if !payload.is_empty() {
234 match self.read_exact(&mut payload) {
235 Ok(()) => {}
236 Err(WalError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
237 return Err(WalError::Io(std::io::Error::new(
238 std::io::ErrorKind::UnexpectedEof,
239 "torn write: incomplete payload",
240 )));
241 }
242 Err(e) => return Err(e),
243 }
244 }
245
246 let checksum_ok = header.crc32c == header.compute_checksum(&payload);
250 if !checksum_ok {
251 if let Some(dwb) = &mut self.double_write
253 && let Ok(Some(recovered)) = dwb.recover_record(header.lsn)
254 {
255 let payload_start =
263 self.offset.checked_sub(payload_len as u64).ok_or_else(|| {
264 WalError::Io(std::io::Error::new(
265 std::io::ErrorKind::InvalidData,
266 "WAL lazy-reader payload offset underflow",
267 ))
268 })?;
269 let resume = checked_offset_add(payload_start, recovered.payload.len() as u64)?;
270 self.file.seek(SeekFrom::Start(resume))?;
271 self.offset = resume;
272 return Ok(recovered.payload);
273 }
274 return Err(WalError::Io(std::io::Error::new(
275 std::io::ErrorKind::InvalidData,
276 "checksum mismatch",
277 )));
278 }
279
280 Ok(payload)
281 }
282
283 pub fn read_record(&mut self, header: &RecordHeader) -> Result<WalRecord> {
289 let raw = self.read_raw_payload(header)?;
290 let record = WalRecord {
291 header: *header,
292 payload: raw,
293 };
294 self.decryptor.decrypt_record(record)
295 }
296
297 pub fn skip_payload(&mut self, header: &RecordHeader) -> Result<()> {
301 header.validate(self.offset)?;
302 let len = u64::from(header.payload_len);
303 let target = checked_offset_add(self.offset, len)?;
304 let file_len = self.file.metadata()?.len();
305 if target > file_len {
306 return Err(WalError::Io(std::io::Error::new(
307 std::io::ErrorKind::UnexpectedEof,
308 "torn write: incomplete payload while skipping",
309 )));
310 }
311 if len > 0 {
312 let signed_len = i64::try_from(len).map_err(|_| {
313 WalError::Io(std::io::Error::new(
314 std::io::ErrorKind::InvalidData,
315 "WAL lazy-reader skip length does not fit i64",
316 ))
317 })?;
318 let position = self.file.seek(SeekFrom::Current(signed_len))?;
319 if position != target {
320 return Err(WalError::Io(std::io::Error::new(
321 std::io::ErrorKind::InvalidData,
322 "WAL lazy-reader seek did not reach bounded target",
323 )));
324 }
325 self.offset = target;
326 }
327 Ok(())
328 }
329
330 pub fn offset(&self) -> u64 {
332 self.offset
333 }
334
335 fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
336 let len = u64::try_from(buf.len()).map_err(|_| {
337 WalError::Io(std::io::Error::new(
338 std::io::ErrorKind::InvalidData,
339 "WAL lazy-reader read length does not fit u64",
340 ))
341 })?;
342 let next_offset = checked_offset_add(self.offset, len)?;
343 self.file.read_exact(buf)?;
344 self.offset = next_offset;
345 Ok(())
346 }
347}
348
349pub fn replay_segment_lazy<F>(path: &Path, keys: Option<&KeyRing>, handler: F) -> Result<()>
355where
356 F: FnMut(&mut LazyWalReader, &RecordHeader) -> Result<()>,
357{
358 replay_one_segment(path, keys, handler).map(|_| ())
359}
360
361fn replay_one_segment<F>(path: &Path, keys: Option<&KeyRing>, mut handler: F) -> Result<u64>
367where
368 F: FnMut(&mut LazyWalReader, &RecordHeader) -> Result<()>,
369{
370 let mut reader = LazyWalReader::open(path, keys)?;
371 let mut last_lsn = 0u64;
372 while let Some(header) = reader
373 .next_header()
374 .map_err(|error| error.with_segment_path(path))?
375 {
376 last_lsn = header.lsn;
377 handler(&mut reader, &header)?;
378 }
379 crate::torn_tail::verify_committed_prefix(path, reader.stop_reason(), last_lsn)?;
380 Ok(last_lsn)
381}
382
383pub fn replay_all_segments_lazy<F>(
388 wal_dir: &Path,
389 keys: Option<&KeyRing>,
390 mut handler: F,
391) -> Result<()>
392where
393 F: FnMut(&mut LazyWalReader, &RecordHeader) -> Result<()>,
394{
395 let segments = crate::segment::discover_segments(wal_dir)?;
396 let mut continuity = crate::segment::SegmentContinuity::new();
397 for seg in &segments {
398 continuity.check(seg)?;
399 let last_lsn = replay_one_segment(&seg.path, keys, &mut handler)?;
400 continuity.completed(seg, last_lsn);
401 }
402 Ok(())
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use crate::record::RecordType;
409 use crate::writer::WalWriter;
410
411 #[test]
412 fn lazy_read_all_payloads() {
413 let dir = tempfile::tempdir().unwrap();
414 let path = dir.path().join("test.wal");
415
416 {
417 let mut w = WalWriter::open_without_direct_io(&path).unwrap();
418 w.append(RecordType::Put as u32, 1, 0, 0, b"hello").unwrap();
419 w.append(RecordType::VectorPut as u32, 1, 0, 0, b"vector-data")
420 .unwrap();
421 w.append(RecordType::Put as u32, 2, 1, 0, b"world").unwrap();
422 w.sync().unwrap();
423 }
424
425 let mut reader = LazyWalReader::open(&path, None).unwrap();
426 let mut records = Vec::new();
427 while let Some(header) = reader.next_header().unwrap() {
428 let payload = reader.read_payload(&header).unwrap();
429 records.push((header, payload));
430 }
431 assert_eq!(records.len(), 3);
432 assert_eq!(records[0].1, b"hello");
433 assert_eq!(records[1].1, b"vector-data");
434 assert_eq!(records[2].1, b"world");
435 }
436
437 #[test]
438 fn lazy_skip_non_matching() {
439 let dir = tempfile::tempdir().unwrap();
440 let path = dir.path().join("test.wal");
441
442 {
443 let mut w = WalWriter::open_without_direct_io(&path).unwrap();
444 w.append(RecordType::TimeseriesBatch as u32, 1, 0, 0, &[0u8; 10000])
446 .unwrap();
447 w.append(RecordType::TimeseriesBatch as u32, 1, 0, 0, &[0u8; 10000])
448 .unwrap();
449 w.append(RecordType::VectorPut as u32, 1, 0, 0, b"small-vec")
450 .unwrap();
451 w.append(RecordType::TimeseriesBatch as u32, 1, 0, 0, &[0u8; 10000])
452 .unwrap();
453 w.sync().unwrap();
454 }
455
456 let mut reader = LazyWalReader::open(&path, None).unwrap();
458 let mut vector_payloads = Vec::new();
459 let mut skipped = 0;
460
461 while let Some(header) = reader.next_header().unwrap() {
462 let rt = RecordType::from_raw(header.record_type);
463 if rt == Some(RecordType::VectorPut) {
464 let payload = reader.read_payload(&header).unwrap();
465 vector_payloads.push(payload);
466 } else {
467 reader.skip_payload(&header).unwrap();
468 skipped += 1;
469 }
470 }
471
472 assert_eq!(vector_payloads.len(), 1);
473 assert_eq!(vector_payloads[0], b"small-vec");
474 assert_eq!(skipped, 3);
475 }
476
477 #[test]
478 fn oversized_payload_header_is_a_typed_error_before_skip_or_allocation() {
479 let dir = tempfile::tempdir().unwrap();
480 let path = dir.path().join("oversized.wal");
481 let header = RecordHeader {
482 magic: crate::record::WAL_MAGIC,
483 format_version: crate::record::WAL_FORMAT_VERSION,
484 record_type: RecordType::Put as u32,
485 lsn: 1,
486 tenant_id: 1,
487 vshard_id: 0,
488 payload_len: (MAX_WAL_PAYLOAD_SIZE + 1) as u32,
489 database_id: 0,
490 apply_key: 0,
491 event_source: crate::record::NO_EVENT_SOURCE,
492 commit_hlc: 0,
493 crc32c: 0,
494 };
495 std::fs::write(&path, header.to_bytes()).unwrap();
496
497 let mut reader = LazyWalReader::open(&path, None).unwrap();
498 assert!(matches!(
499 reader.next_header(),
500 Err(WalError::PayloadTooLarge { .. })
501 ));
502 }
503
504 #[test]
505 fn skip_payload_rejects_truncated_tail_without_seeking_beyond_eof() {
506 let dir = tempfile::tempdir().unwrap();
507 let path = dir.path().join("truncated.wal");
508 let mut writer = WalWriter::open_without_direct_io(&path).unwrap();
509 writer
510 .append(RecordType::Put as u32, 1, 0, 0, b"payload")
511 .unwrap();
512 writer.sync().unwrap();
513 drop(writer);
514 let length = std::fs::metadata(&path).unwrap().len();
515 std::fs::OpenOptions::new()
516 .write(true)
517 .open(&path)
518 .unwrap()
519 .set_len(length - 1)
520 .unwrap();
521
522 let mut reader = LazyWalReader::open(&path, None).unwrap();
523 let header = reader.next_header().unwrap().expect("header");
524 assert!(
525 matches!(reader.skip_payload(&header), Err(WalError::Io(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof)
526 );
527 assert_eq!(reader.offset(), HEADER_SIZE as u64);
528 }
529
530 #[test]
531 fn checked_offset_add_rejects_overflow() {
532 assert!(checked_offset_add(u64::MAX, 1).is_err());
533 }
534
535 #[test]
536 fn replay_all_segments_lazy_works() {
537 let dir = tempfile::tempdir().unwrap();
538 let path = dir.path().join("wal-00000000000000000001.seg");
540
541 {
542 let mut w = WalWriter::open_without_direct_io(&path).unwrap();
543 w.append(RecordType::Put as u32, 1, 0, 0, b"a").unwrap();
544 w.append(RecordType::Put as u32, 1, 0, 0, b"b").unwrap();
545 w.sync().unwrap();
546 }
547
548 let mut count = 0;
549 replay_all_segments_lazy(dir.path(), None, |reader, header| {
550 reader.skip_payload(header)?;
551 count += 1;
552 Ok(())
553 })
554 .unwrap();
555 assert_eq!(count, 2);
556 }
557
558 #[test]
559 fn empty_wal_no_records() {
560 let dir = tempfile::tempdir().unwrap();
561 let path = dir.path().join("empty.wal");
562
563 {
564 let mut w = WalWriter::open_without_direct_io(&path).unwrap();
565 w.sync().unwrap();
566 }
567
568 let mut reader = LazyWalReader::open(&path, None).unwrap();
569 assert!(reader.next_header().unwrap().is_none());
570 }
571
572 #[test]
575 fn an_unknown_format_version_at_the_first_record_is_an_error() {
576 let dir = tempfile::tempdir().unwrap();
577 let path = dir.path().join("unknown.wal");
578 {
579 let mut writer = WalWriter::open_without_direct_io(&path).unwrap();
580 writer
581 .append(RecordType::Put as u32, 1, 0, 0, b"x")
582 .unwrap();
583 writer.sync().unwrap();
584 }
585
586 let mut bytes = std::fs::read(&path).unwrap();
587 crate::record::foreign_version::reseal_with_version(
588 &mut bytes,
589 0,
590 crate::record::WAL_FORMAT_VERSION + 1,
591 );
592 std::fs::write(&path, &bytes).unwrap();
593
594 let mut reader = LazyWalReader::open(&path, None).unwrap();
595 match reader.next_header() {
596 Err(WalError::UnsupportedVersion { version, supported }) => {
597 assert_eq!(version, crate::record::WAL_FORMAT_VERSION + 1);
598 assert_eq!(supported, crate::record::WAL_FORMAT_VERSION);
599 }
600 other => panic!("expected UnsupportedVersion, got {other:?}"),
601 }
602 }
603
604 #[test]
607 fn a_version_mismatch_after_the_first_record_is_damage() {
608 let dir = tempfile::tempdir().unwrap();
609 let path = dir.path().join("two-records.wal");
610 {
611 let mut writer = WalWriter::open_without_direct_io(&path).unwrap();
612 writer
613 .append(RecordType::Put as u32, 1, 0, 0, b"first")
614 .unwrap();
615 writer
616 .append(RecordType::Put as u32, 1, 0, 0, b"second")
617 .unwrap();
618 writer.sync().unwrap();
619 }
620
621 let mut reader = LazyWalReader::open(&path, None).unwrap();
622 let first = reader.next_header().unwrap().expect("the first header");
623 reader.skip_payload(&first).unwrap();
624 let second_at = reader.offset();
625
626 let mut bytes = std::fs::read(&path).unwrap();
627 let version_at = usize::try_from(second_at).unwrap() + 4;
628 bytes[version_at..version_at + 2]
629 .copy_from_slice(&(crate::record::WAL_FORMAT_VERSION + 1).to_le_bytes());
630 std::fs::write(&path, &bytes).unwrap();
631
632 let mut reader = LazyWalReader::open(&path, None).unwrap();
633 let first = reader
634 .next_header()
635 .unwrap()
636 .expect("the first record is still readable");
637 assert_eq!(first.lsn, 1);
638 reader.skip_payload(&first).unwrap();
639 assert!(
640 reader.next_header().unwrap().is_none(),
641 "the mismatch stops the stream instead of failing the read"
642 );
643 assert_eq!(
644 reader.stop_reason(),
645 Some(StopReason::Corruption { offset: second_at }),
646 "a mismatch after the first record is damage, not a format gap"
647 );
648 }
649}