1use std::fs;
20use std::path::{Path, PathBuf};
21
22use tracing::info;
23
24use crate::crypto::KeyRing;
25use crate::error::{Result, WalError};
26use crate::record::{RecordTarget, WalRecord};
27use crate::segment::orphan::discard_unused_segment;
28use crate::segment::{
29 DEFAULT_SEGMENT_TARGET_SIZE, SegmentContinuity, SegmentMeta, TruncateResult,
30 check_resume_above_previous, check_retained_floor, discover_segments, segment_path,
31 truncate_segments,
32};
33use crate::writer::{WalWriter, WalWriterConfig};
34
35#[derive(Debug, Clone)]
37pub struct SegmentedWalConfig {
38 pub wal_dir: PathBuf,
40
41 pub segment_target_size: u64,
45
46 pub writer_config: WalWriterConfig,
48}
49
50impl SegmentedWalConfig {
51 pub fn new(wal_dir: PathBuf) -> Self {
53 Self {
54 wal_dir,
55 segment_target_size: DEFAULT_SEGMENT_TARGET_SIZE,
56 writer_config: WalWriterConfig::default(),
57 }
58 }
59
60 pub fn for_testing(wal_dir: PathBuf) -> Self {
62 Self {
63 wal_dir,
64 segment_target_size: DEFAULT_SEGMENT_TARGET_SIZE,
65 writer_config: WalWriterConfig {
66 use_direct_io: false,
67 ..Default::default()
68 },
69 }
70 }
71}
72
73pub struct SegmentedWal {
79 wal_dir: PathBuf,
81
82 writer: WalWriter,
84
85 active_first_lsn: u64,
87
88 segment_target_size: u64,
90
91 writer_config: WalWriterConfig,
93
94 encryption_ring: Option<crate::crypto::KeyRing>,
96}
97
98impl SegmentedWal {
99 pub fn open(config: SegmentedWalConfig) -> Result<Self> {
105 fs::create_dir_all(&config.wal_dir).map_err(WalError::Io)?;
106
107 let segments = discover_segments(&config.wal_dir)?;
108
109 let (writer, active_first_lsn) = if segments.is_empty() {
110 let path = segment_path(&config.wal_dir, 1);
112 let writer = WalWriter::open(&path, config.writer_config.clone())?;
113 crate::segment::fsync_directory(&config.wal_dir)?;
118 (writer, 1u64)
119 } else {
120 let last = &segments[segments.len() - 1];
126 let writer =
127 WalWriter::open_resuming(&last.path, config.writer_config.clone(), last.first_lsn)?;
128 check_resume_above_previous(&segments, writer.next_lsn())?;
129 (writer, last.first_lsn)
130 };
131
132 info!(
133 wal_dir = %config.wal_dir.display(),
134 segments = segments.len().max(1),
135 active_first_lsn,
136 next_lsn = writer.next_lsn(),
137 "segmented WAL opened"
138 );
139
140 Ok(Self {
141 wal_dir: config.wal_dir,
142 writer,
143 active_first_lsn,
144 segment_target_size: config.segment_target_size,
145 writer_config: config.writer_config,
146 encryption_ring: None,
147 })
148 }
149
150 pub fn set_encryption_ring(&mut self, ring: crate::crypto::KeyRing) -> Result<()> {
155 self.writer.set_encryption_ring(ring.clone())?;
156 self.encryption_ring = Some(ring);
157 Ok(())
158 }
159
160 pub fn configure_encryption_ring(&mut self, ring: crate::crypto::KeyRing) -> Result<()> {
163 if self.writer.can_set_encryption_ring() {
164 self.set_encryption_ring(ring)
165 } else {
166 self.rotate_encryption_ring(ring)
167 }
168 }
169
170 pub fn encryption_ring(&self) -> Option<&crate::crypto::KeyRing> {
172 self.encryption_ring.as_ref()
173 }
174
175 pub fn rotate_encryption_ring(&mut self, ring: crate::crypto::KeyRing) -> Result<()> {
179 self.roll_segment_with_ring(Some(ring))
180 }
181
182 pub fn append(
190 &mut self,
191 record_type: u32,
192 tenant_id: u64,
193 vshard_id: u32,
194 database_id: u64,
195 payload: &[u8],
196 ) -> Result<u64> {
197 self.append_keyed(
198 RecordTarget {
199 record_type,
200 tenant_id,
201 vshard_id,
202 database_id,
203 event_source: crate::record::NO_EVENT_SOURCE,
204 commit_hlc: 0,
205 },
206 payload,
207 0,
208 )
209 }
210
211 pub fn append_keyed(
215 &mut self,
216 target: RecordTarget,
217 payload: &[u8],
218 apply_key: u64,
219 ) -> Result<u64> {
220 if self.writer.file_offset() >= self.segment_target_size {
222 self.roll_segment()?;
223 }
224
225 self.writer.append_keyed(target, payload, apply_key)
226 }
227
228 pub fn sync(&mut self) -> Result<()> {
230 self.writer.sync()
231 }
232
233 pub fn seal_active_segment(&mut self) -> Result<()> {
237 if self.writer.next_lsn() == self.active_first_lsn {
238 return Ok(());
239 }
240 self.roll_segment()
241 }
242
243 pub fn next_lsn(&self) -> u64 {
245 self.writer.next_lsn()
246 }
247
248 pub fn max_payload(&self) -> usize {
250 self.writer.max_payload()
251 }
252
253 pub fn active_segment_first_lsn(&self) -> u64 {
255 self.active_first_lsn
256 }
257
258 pub fn wal_dir(&self) -> &Path {
260 &self.wal_dir
261 }
262
263 pub fn truncate_before(&self, checkpoint_lsn: u64) -> Result<TruncateResult> {
270 truncate_segments(&self.wal_dir, checkpoint_lsn, self.active_first_lsn)
271 }
272
273 pub fn replay(&self) -> Result<Vec<WalRecord>> {
278 replay_all_segments(&self.wal_dir, self.encryption_ring.as_ref())
279 }
280
281 pub fn replay_from(&self, from_lsn: u64) -> Result<Vec<WalRecord>> {
292 Ok(replay_from_limit_dir(
293 &self.wal_dir,
294 from_lsn,
295 usize::MAX,
296 self.encryption_ring.as_ref(),
297 )?
298 .0)
299 }
300
301 pub fn replay_from_limit(
303 &self,
304 from_lsn: u64,
305 max_records: usize,
306 ) -> Result<(Vec<WalRecord>, bool)> {
307 replay_from_limit_dir(
308 &self.wal_dir,
309 from_lsn,
310 max_records,
311 self.encryption_ring.as_ref(),
312 )
313 }
314
315 pub fn list_segments(&self) -> Result<Vec<SegmentMeta>> {
317 discover_segments(&self.wal_dir)
318 }
319
320 pub fn total_size_bytes(&self) -> Result<u64> {
322 let segments = discover_segments(&self.wal_dir)?;
323 Ok(segments.iter().map(|s| s.file_size).sum())
324 }
325
326 fn roll_segment(&mut self) -> Result<()> {
328 let next_ring = self
329 .encryption_ring
330 .as_ref()
331 .map(|ring| {
332 ring.current()
333 .with_fresh_epoch()
334 .map(crate::crypto::KeyRing::new)
335 })
336 .transpose()?;
337 self.roll_segment_with_ring(next_ring)
338 }
339
340 fn roll_segment_with_ring(&mut self, next_ring: Option<crate::crypto::KeyRing>) -> Result<()> {
353 self.writer.sync()?;
357 let new_first_lsn = self.writer.next_lsn();
358 let new_path = segment_path(&self.wal_dir, new_first_lsn);
359 let new_writer = match self.open_and_seal(&new_path, new_first_lsn, next_ring.as_ref()) {
363 Ok(new_writer) => new_writer,
364 Err(roll_err) => {
365 return Err(discard_unused_segment(&self.wal_dir, &new_path, roll_err));
366 }
367 };
368
369 self.encryption_ring = next_ring;
372 self.writer = new_writer;
373 self.active_first_lsn = new_first_lsn;
374
375 info!(
376 segment = %new_path.display(),
377 first_lsn = new_first_lsn,
378 "rolled to new WAL segment"
379 );
380 Ok(())
381 }
382
383 fn open_and_seal(
385 &mut self,
386 new_path: &Path,
387 new_first_lsn: u64,
388 next_ring: Option<&KeyRing>,
389 ) -> Result<WalWriter> {
390 let mut new_writer =
391 WalWriter::open_with_start_lsn(new_path, self.writer_config.clone(), new_first_lsn)?;
392
393 crate::segment::fsync_directory(&self.wal_dir)?;
394
395 if let Some(ring) = next_ring {
396 new_writer.set_encryption_ring(ring.clone())?;
397 }
398
399 nodedb_types::fail_point_err!("wal::roll_before_seal", |detail: String| WalError::Io(
400 std::io::Error::other(format!("failpoint wal::roll_before_seal: {detail}"))
401 ));
402
403 self.writer.seal()?;
408 Ok(new_writer)
409 }
410}
411
412pub fn replay_all_segments(wal_dir: &Path, keys: Option<&KeyRing>) -> Result<Vec<WalRecord>> {
426 Ok(replay_from_limit_dir(wal_dir, 0, usize::MAX, keys)?.0)
427}
428
429pub fn replay_from_limit_dir(
444 wal_dir: &Path,
445 from_lsn: u64,
446 max_records: usize,
447 keys: Option<&KeyRing>,
448) -> Result<(Vec<WalRecord>, bool)> {
449 let segments = discover_segments(wal_dir)?;
450 check_retained_floor(&segments, from_lsn)?;
454 let mut records = Vec::with_capacity(max_records.min(4096));
455 let mut continuity = SegmentContinuity::new();
456
457 for seg in &segments {
458 continuity.check(seg)?;
459 let mut reader = crate::reader::WalReader::open(&seg.path, keys)?;
462 let mut last_lsn = 0u64;
466 while let Some(record) = reader
467 .next_record()
468 .map_err(|error| error.with_segment_path(&seg.path))?
469 {
470 last_lsn = record.header.lsn;
471 if record.header.lsn >= from_lsn {
472 records.push(record);
473 if records.len() >= max_records {
474 return Ok((records, true));
477 }
478 }
479 }
480 crate::torn_tail::verify_committed_prefix(&seg.path, reader.stop_reason(), last_lsn)?;
484 continuity.completed(seg, last_lsn);
485 }
486
487 Ok((records, false))
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493 use crate::record::RecordType;
494
495 fn test_config(dir: &Path) -> SegmentedWalConfig {
496 SegmentedWalConfig::for_testing(dir.to_path_buf())
497 }
498
499 #[test]
500 fn create_and_append() {
501 let dir = tempfile::tempdir().unwrap();
502 let wal_dir = dir.path().join("wal");
503
504 let mut wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
505 let lsn1 = wal
506 .append(RecordType::Put as u32, 1, 0, 0, b"hello")
507 .unwrap();
508 let lsn2 = wal
509 .append(RecordType::Put as u32, 1, 0, 0, b"world")
510 .unwrap();
511 wal.sync().unwrap();
512
513 assert_eq!(lsn1, 1);
514 assert_eq!(lsn2, 2);
515 assert_eq!(wal.next_lsn(), 3);
516 }
517
518 #[test]
519 fn replay_after_close() {
520 let dir = tempfile::tempdir().unwrap();
521 let wal_dir = dir.path().join("wal");
522
523 {
525 let mut wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
526 wal.append(RecordType::Put as u32, 1, 0, 0, b"first")
527 .unwrap();
528 wal.append(RecordType::Delete as u32, 2, 1, 0, b"second")
529 .unwrap();
530 wal.append(RecordType::Put as u32, 1, 0, 0, b"third")
531 .unwrap();
532 wal.sync().unwrap();
533 }
534
535 let wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
537 let records = wal.replay().unwrap();
538 assert_eq!(records.len(), 3);
539 assert_eq!(records[0].payload, b"first");
540 assert_eq!(records[1].payload, b"second");
541 assert_eq!(records[2].payload, b"third");
542 assert_eq!(wal.next_lsn(), 4);
543 }
544
545 #[test]
546 fn automatic_segment_rollover() {
547 let dir = tempfile::tempdir().unwrap();
548 let wal_dir = dir.path().join("wal");
549
550 let config = SegmentedWalConfig {
552 wal_dir: wal_dir.clone(),
553 segment_target_size: 100, writer_config: WalWriterConfig {
555 use_direct_io: false,
556 ..Default::default()
557 },
558 };
559
560 let mut wal = SegmentedWal::open(config).unwrap();
561
562 for i in 0..20u32 {
564 let payload = format!("record-{i:04}");
565 wal.append(RecordType::Put as u32, 1, 0, 0, payload.as_bytes())
566 .unwrap();
567 wal.sync().unwrap();
568 }
569
570 let segments = wal.list_segments().unwrap();
572 assert!(
573 segments.len() > 1,
574 "expected multiple segments, got {}",
575 segments.len()
576 );
577
578 let records = wal.replay().unwrap();
580 assert_eq!(records.len(), 20);
581 for (i, record) in records.iter().enumerate() {
582 assert_eq!(record.header.lsn, (i + 1) as u64);
583 let expected = format!("record-{i:04}");
584 assert_eq!(record.payload, expected.as_bytes());
585 }
586 }
587
588 #[test]
589 fn truncation_removes_old_segments() {
590 let dir = tempfile::tempdir().unwrap();
591 let wal_dir = dir.path().join("wal");
592
593 let config = SegmentedWalConfig {
594 wal_dir: wal_dir.clone(),
595 segment_target_size: 100,
596 writer_config: WalWriterConfig {
597 use_direct_io: false,
598 ..Default::default()
599 },
600 };
601
602 let mut wal = SegmentedWal::open(config).unwrap();
603
604 for i in 0..20u32 {
606 let payload = format!("record-{i:04}");
607 wal.append(RecordType::Put as u32, 1, 0, 0, payload.as_bytes())
608 .unwrap();
609 wal.sync().unwrap();
610 }
611
612 let segments_before = wal.list_segments().unwrap();
613 assert!(segments_before.len() > 1);
614
615 let result = wal.truncate_before(15).unwrap();
617 assert!(result.segments_deleted > 0);
618 assert!(result.bytes_reclaimed > 0);
619
620 let segments_after = wal.list_segments().unwrap();
621 assert!(segments_after.len() < segments_before.len());
622
623 let records = wal.replay().unwrap();
625 assert!(records.iter().any(|r| r.header.lsn >= 15));
627 }
628
629 #[test]
630 fn replay_from_checkpoint_lsn() {
631 let dir = tempfile::tempdir().unwrap();
632 let wal_dir = dir.path().join("wal");
633
634 let mut wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
635 for i in 0..10u32 {
636 wal.append(RecordType::Put as u32, 1, 0, 0, format!("r{i}").as_bytes())
637 .unwrap();
638 }
639 wal.sync().unwrap();
640
641 let records = wal.replay_from(6).unwrap();
643 assert_eq!(records.len(), 5);
644 assert_eq!(records[0].header.lsn, 6);
645 assert_eq!(records[4].header.lsn, 10);
646 }
647
648 fn truncated_wal(wal_dir: &Path) -> (SegmentedWal, u64) {
651 let config = SegmentedWalConfig {
652 wal_dir: wal_dir.to_path_buf(),
653 segment_target_size: 100,
654 writer_config: WalWriterConfig {
655 use_direct_io: false,
656 ..Default::default()
657 },
658 };
659
660 let mut wal = SegmentedWal::open(config).expect("open");
661 for i in 0..40u32 {
662 wal.append(
663 RecordType::Put as u32,
664 1,
665 0,
666 0,
667 format!("record-{i:04}").as_bytes(),
668 )
669 .expect("append");
670 wal.sync().expect("sync");
671 }
672
673 let result = wal.truncate_before(20).expect("truncate");
674 assert!(
675 result.segments_deleted > 0,
676 "the test needs a truncated prefix to have a floor above LSN 1"
677 );
678
679 let floor = wal.list_segments().expect("list")[0].first_lsn;
680 assert!(floor > 1, "expected the retained floor to have moved up");
681 (wal, floor)
682 }
683
684 #[test]
685 fn replay_from_the_retained_floor_returns_the_whole_suffix() {
686 let dir = tempfile::tempdir().unwrap();
687 let (wal, floor) = truncated_wal(&dir.path().join("wal"));
688
689 let records = wal.replay_from(floor).expect("floor is fully retained");
690 assert_eq!(records[0].header.lsn, floor);
691 assert_eq!(records.last().unwrap().header.lsn, 40);
692 assert_eq!(records.len() as u64, 40 - floor + 1);
693 }
694
695 #[test]
698 fn replay_from_exactly_the_floor_is_not_an_error() {
699 let dir = tempfile::tempdir().unwrap();
700 let (wal, floor) = truncated_wal(&dir.path().join("wal"));
701
702 assert!(wal.replay_from(floor).is_ok());
703 assert!(wal.replay_from(floor - 1).is_err());
705 }
706
707 #[test]
708 fn replay_below_the_retained_floor_is_a_typed_error_naming_both_lsns() {
709 let dir = tempfile::tempdir().unwrap();
710 let (wal, floor) = truncated_wal(&dir.path().join("wal"));
711
712 match wal.replay_from(1) {
713 Err(WalError::ReplayBelowRetainedFloor {
714 from_lsn,
715 retained_floor_lsn,
716 ..
717 }) => {
718 assert_eq!(from_lsn, 1);
719 assert_eq!(retained_floor_lsn, floor);
720 }
721 other => panic!("expected a retained-floor violation, got {other:?}"),
722 }
723
724 assert!(matches!(
727 wal.replay_from_limit(1, 5),
728 Err(WalError::ReplayBelowRetainedFloor { .. })
729 ));
730
731 assert!(wal.replay().is_ok());
733 }
734
735 #[test]
736 fn total_size_bytes() {
737 let dir = tempfile::tempdir().unwrap();
738 let wal_dir = dir.path().join("wal");
739
740 let mut wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
741 wal.append(RecordType::Put as u32, 1, 0, 0, b"data")
742 .unwrap();
743 wal.sync().unwrap();
744
745 let size = wal.total_size_bytes().unwrap();
746 assert!(size > 0);
747 }
748
749 #[test]
750 fn reopen_continues_lsn() {
751 let dir = tempfile::tempdir().unwrap();
752 let wal_dir = dir.path().join("wal");
753
754 {
755 let mut wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
756 wal.append(RecordType::Put as u32, 1, 0, 0, b"a").unwrap();
757 wal.append(RecordType::Put as u32, 1, 0, 0, b"b").unwrap();
758 wal.sync().unwrap();
759 }
760
761 {
762 let mut wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
763 assert_eq!(wal.next_lsn(), 3);
764 let lsn = wal.append(RecordType::Put as u32, 1, 0, 0, b"c").unwrap();
765 assert_eq!(lsn, 3);
766 wal.sync().unwrap();
767 }
768
769 let wal = SegmentedWal::open(test_config(&wal_dir)).unwrap();
770 let records = wal.replay().unwrap();
771 assert_eq!(records.len(), 3);
772 }
773
774 #[test]
775 fn encryption_persists_across_segments() {
776 let dir = tempfile::tempdir().unwrap();
785 let wal_dir = dir.path().join("wal");
786 let key_bytes = [42u8; 32];
787
788 let config = SegmentedWalConfig {
789 wal_dir: wal_dir.clone(),
790 segment_target_size: 100,
791 writer_config: WalWriterConfig {
792 use_direct_io: false,
793 ..Default::default()
794 },
795 };
796
797 {
799 let key = crate::crypto::WalEncryptionKey::from_bytes(&key_bytes).unwrap();
800 let ring = crate::crypto::KeyRing::new(key);
801 let mut wal = SegmentedWal::open(config.clone()).unwrap();
802 wal.set_encryption_ring(ring).unwrap();
803
804 for i in 0..10u32 {
805 wal.append(
806 RecordType::Put as u32,
807 1,
808 0,
809 0,
810 format!("enc-{i}").as_bytes(),
811 )
812 .unwrap();
813 wal.sync().unwrap();
814 }
815 assert!(wal.list_segments().unwrap().len() > 1);
816 }
817
818 let segments = crate::segment::discover_segments(&wal_dir).unwrap();
820 assert!(
821 segments.len() > 1,
822 "expected multiple segments after rollover"
823 );
824
825 let key_for_read = crate::crypto::WalEncryptionKey::from_bytes(&key_bytes).unwrap();
826 let ring_for_read = crate::crypto::KeyRing::new(key_for_read);
827
828 let mut all_payloads = Vec::new();
829
830 for seg in &segments {
833 let reader = crate::reader::WalReader::open_raw(&seg.path).unwrap();
834 let epoch = *reader
836 .segment_preamble()
837 .expect("encrypted segment must have a preamble")
838 .epoch();
839 let preamble_bytes = reader.segment_preamble().unwrap().to_bytes();
840
841 for record_result in reader.records() {
842 let record = record_result.unwrap();
843 assert!(record.is_encrypted(), "all records should be encrypted");
844 let plaintext = record
845 .decrypt_payload_ring(&epoch, Some(&preamble_bytes), Some(&ring_for_read))
846 .unwrap();
847 all_payloads.push(plaintext);
848 }
849 }
850
851 assert_eq!(all_payloads.len(), 10);
852 for (i, payload) in all_payloads.iter().enumerate() {
853 assert_eq!(payload, format!("enc-{i}").as_bytes());
854 }
855 }
856
857 #[test]
863 fn wal_encrypted_restart_roundtrip() {
864 let dir = tempfile::tempdir().unwrap();
865 let wal_dir = dir.path().join("wal");
866 let key_bytes = [0xABu8; 32];
867
868 let config = SegmentedWalConfig::for_testing(wal_dir.clone());
869
870 {
872 let key = crate::crypto::WalEncryptionKey::from_bytes(&key_bytes).unwrap();
873 let ring = crate::crypto::KeyRing::new(key);
874 let mut wal = SegmentedWal::open(config.clone()).unwrap();
875 wal.set_encryption_ring(ring).unwrap();
876
877 for i in 0..5u32 {
878 wal.append(
879 RecordType::Put as u32,
880 1,
881 0,
882 0,
883 format!("restart-{i}").as_bytes(),
884 )
885 .unwrap();
886 }
887 wal.sync().unwrap();
888 }
889
890 let key_restart = crate::crypto::WalEncryptionKey::from_bytes(&key_bytes).unwrap();
892 let ring_restart = crate::crypto::KeyRing::new(key_restart);
893
894 let segments = crate::segment::discover_segments(&wal_dir).unwrap();
896 let mut payloads = Vec::new();
897
898 for seg in &segments {
899 let reader = crate::reader::WalReader::open_raw(&seg.path).unwrap();
900 let epoch = *reader
901 .segment_preamble()
902 .expect("segment must have preamble after encrypted write")
903 .epoch();
904 let preamble_bytes = reader.segment_preamble().unwrap().to_bytes();
905
906 for record_result in reader.records() {
907 let record = record_result.unwrap();
908 let pt = record
909 .decrypt_payload_ring(&epoch, Some(&preamble_bytes), Some(&ring_restart))
910 .unwrap();
911 payloads.push(pt);
912 }
913 }
914
915 assert_eq!(payloads.len(), 5);
916 for (i, pt) in payloads.iter().enumerate() {
917 assert_eq!(pt, format!("restart-{i}").as_bytes());
918 }
919 }
920
921 #[test]
926 fn epoch_tamper_rejected() {
927 let dir = tempfile::tempdir().unwrap();
928 let wal_dir = dir.path().join("wal");
929 let key_bytes = [0x55u8; 32];
930
931 let config = SegmentedWalConfig::for_testing(wal_dir.clone());
932
933 {
934 let key = crate::crypto::WalEncryptionKey::from_bytes(&key_bytes).unwrap();
935 let ring = crate::crypto::KeyRing::new(key);
936 let mut wal = SegmentedWal::open(config).unwrap();
937 wal.set_encryption_ring(ring).unwrap();
938 wal.append(RecordType::Put as u32, 1, 0, 0, b"sensitive payload")
939 .unwrap();
940 wal.sync().unwrap();
941 }
942
943 let segments = crate::segment::discover_segments(&wal_dir).unwrap();
945 assert_eq!(segments.len(), 1);
946 let seg_path = &segments[0].path;
947
948 let mut raw = std::fs::read(seg_path).unwrap();
949 raw[9] ^= 0xFF;
951 std::fs::write(seg_path, &raw).unwrap();
952
953 let key_read = crate::crypto::WalEncryptionKey::from_bytes(&key_bytes).unwrap();
957 let ring_read = crate::crypto::KeyRing::new(key_read);
958
959 let reader = crate::reader::WalReader::open_raw(seg_path).unwrap();
960 let epoch = *reader.segment_preamble().unwrap().epoch();
961 let preamble_bytes = reader.segment_preamble().unwrap().to_bytes();
962
963 let record = reader.records().next().unwrap().unwrap();
964 let result = record.decrypt_payload_ring(&epoch, Some(&preamble_bytes), Some(&ring_read));
965 assert!(
966 result.is_err(),
967 "decryption with tampered preamble epoch must fail"
968 );
969 }
970
971 #[test]
972 fn replay_from_limit_basic() {
973 let dir = tempfile::tempdir().unwrap();
974 let config = test_config(dir.path());
975 let mut wal = SegmentedWal::open(config).unwrap();
976
977 for i in 0..10u8 {
979 wal.append(RecordType::Put as u32, 1, 0, 0, &[i]).unwrap();
980 }
981 wal.sync().unwrap();
982
983 let (records, has_more) = wal.replay_from_limit(1, 100).unwrap();
985 assert_eq!(records.len(), 10);
986 assert!(!has_more);
987
988 let (records, has_more) = wal.replay_from_limit(1, 3).unwrap();
990 assert_eq!(records.len(), 3);
991 assert!(has_more);
992 assert_eq!(records[0].header.lsn, 1);
993 assert_eq!(records[2].header.lsn, 3);
994 }
995
996 #[test]
997 fn replay_from_limit_with_lsn_filter() {
998 let dir = tempfile::tempdir().unwrap();
999 let config = test_config(dir.path());
1000 let mut wal = SegmentedWal::open(config).unwrap();
1001
1002 for i in 0..10u8 {
1003 wal.append(RecordType::Put as u32, 1, 0, 0, &[i]).unwrap();
1004 }
1005 wal.sync().unwrap();
1006
1007 let (records, has_more) = wal.replay_from_limit(6, 100).unwrap();
1009 assert_eq!(records.len(), 5);
1010 assert!(!has_more);
1011 assert_eq!(records[0].header.lsn, 6);
1012
1013 let (records, has_more) = wal.replay_from_limit(6, 2).unwrap();
1015 assert_eq!(records.len(), 2);
1016 assert!(has_more);
1017 }
1018
1019 #[test]
1020 fn replay_from_limit_empty() {
1021 let dir = tempfile::tempdir().unwrap();
1022 let config = test_config(dir.path());
1023 let mut wal = SegmentedWal::open(config).unwrap();
1024
1025 wal.append(RecordType::Put as u32, 1, 0, 0, b"a").unwrap();
1026 wal.sync().unwrap();
1027
1028 let (records, has_more) = wal.replay_from_limit(999, 100).unwrap();
1030 assert!(records.is_empty());
1031 assert!(!has_more);
1032 }
1033
1034 #[test]
1035 fn replay_from_limit_across_segments() {
1036 let dir = tempfile::tempdir().unwrap();
1037 let config = test_config(dir.path());
1038 let mut wal = SegmentedWal::open(config).unwrap();
1039
1040 for i in 0..10u8 {
1042 wal.append(RecordType::Put as u32, 1, 0, 0, &[i]).unwrap();
1043 }
1044 wal.sync().unwrap();
1045 wal.roll_segment().unwrap();
1047
1048 for i in 10..20u8 {
1050 wal.append(RecordType::Put as u32, 1, 0, 0, &[i]).unwrap();
1051 }
1052 wal.sync().unwrap();
1053
1054 let seg_count = wal.list_segments().unwrap().len();
1055 assert!(
1056 seg_count >= 2,
1057 "expected multiple segments, got {seg_count}"
1058 );
1059
1060 let (records, has_more) = wal.replay_from_limit(1, 5).unwrap();
1062 assert_eq!(records.len(), 5);
1063 assert!(has_more);
1064
1065 let next_lsn = records.last().unwrap().header.lsn + 1;
1067 let (records2, _) = wal.replay_from_limit(next_lsn, 200).unwrap();
1068 assert_eq!(records2.len(), 15); }
1070
1071 #[test]
1074 fn time_anchors_survive_rollover_and_restart() {
1075 use std::sync::Arc;
1076
1077 use crate::time_anchors::TimeAnchors;
1078
1079 let dir = tempfile::tempdir().unwrap();
1080 let wal_dir = dir.path().join("wal");
1081 let config = |anchors: &Arc<TimeAnchors>| SegmentedWalConfig {
1082 wal_dir: wal_dir.clone(),
1083 segment_target_size: 200,
1084 writer_config: WalWriterConfig {
1085 use_direct_io: false,
1086 time_anchors: Some(Arc::clone(anchors)),
1087 ..Default::default()
1088 },
1089 };
1090
1091 let live = Arc::new(TimeAnchors::new(Arc::new(nodedb_types::HlcClock::new())));
1092 {
1093 let mut wal = SegmentedWal::open(config(&live)).unwrap();
1094 for i in 0..12u32 {
1095 wal.append(RecordType::Put as u32, 1, 0, 0, format!("r{i}").as_bytes())
1096 .unwrap();
1097 if i % 3 == 2 {
1098 wal.sync().unwrap();
1099 }
1100 }
1101 wal.sync().unwrap();
1102 assert!(wal.list_segments().unwrap().len() > 1);
1103 }
1104 let live_anchors = live.anchors();
1105 assert!(live_anchors.len() >= 4);
1106
1107 let records = replay_all_segments(&wal_dir, None).unwrap();
1108 let lsns: Vec<u64> = records.iter().map(|r| r.header.lsn).collect();
1109 assert!(lsns.windows(2).all(|w| w[1] == w[0] + 1), "{lsns:?}");
1110 let puts = records
1111 .iter()
1112 .filter(|r| RecordType::from_raw(r.logical_record_type()) == Some(RecordType::Put))
1113 .count();
1114 assert_eq!(puts, 12);
1115
1116 let restarted = TimeAnchors::new(Arc::new(nodedb_types::HlcClock::new()));
1117 restarted.absorb_replayed(&records).unwrap();
1118 assert_eq!(restarted.anchors(), live_anchors);
1119 }
1120
1121 #[cfg(feature = "failpoints")]
1124 #[test]
1125 fn an_injected_seal_failure_leaves_no_new_segment_file() {
1126 use nodedb_types::fail_point::FailGuard;
1127
1128 let dir = tempfile::tempdir().unwrap();
1129 let mut wal = SegmentedWal::open(test_config(dir.path())).unwrap();
1130 wal.append(RecordType::Put as u32, 1, 0, 0, b"a").unwrap();
1131 wal.sync().unwrap();
1132 let before = wal.list_segments().unwrap();
1133 {
1134 let _guard = FailGuard::fail("wal::roll_before_seal", "injected");
1135 assert!(wal.roll_segment().is_err());
1136 }
1137 assert_eq!(wal.list_segments().unwrap(), before);
1138 assert_eq!(wal.active_segment_first_lsn(), 1);
1139
1140 wal.append(RecordType::Put as u32, 1, 0, 0, b"b").unwrap();
1141 wal.roll_segment().unwrap();
1142 assert_eq!(wal.list_segments().unwrap().len(), 2);
1143 assert_eq!(wal.active_segment_first_lsn(), 3);
1144 }
1145
1146 #[test]
1150 fn open_refuses_a_segment_left_by_an_interrupted_roll() {
1151 let dir = tempfile::tempdir().unwrap();
1152 {
1153 let mut wal = SegmentedWal::open(test_config(dir.path())).unwrap();
1154 for i in 0..5u8 {
1155 wal.append(RecordType::Put as u32, 1, 0, 0, &[i]).unwrap();
1156 }
1157 wal.sync().unwrap();
1158 }
1159 let orphan = segment_path(dir.path(), 3);
1160 WalWriter::open_with_start_lsn(&orphan, test_config(dir.path()).writer_config, 3).unwrap();
1161
1162 match SegmentedWal::open(test_config(dir.path())) {
1163 Err(WalError::SegmentOverlapsPrevious {
1164 first_lsn,
1165 previous_last_lsn,
1166 ..
1167 }) => {
1168 assert_eq!(first_lsn, 3);
1169 assert_eq!(previous_last_lsn, 5);
1170 }
1171 Err(other) => panic!("expected SegmentOverlapsPrevious, got {other}"),
1172 Ok(_) => panic!("opened a WAL whose last segment reissues LSNs"),
1173 }
1174 }
1175
1176 #[test]
1181 fn open_accepts_a_newest_segment_with_a_zeroed_version() {
1182 let dir = tempfile::tempdir().unwrap();
1183 let next = {
1184 let mut wal = SegmentedWal::open(test_config(dir.path())).unwrap();
1185 wal.append(RecordType::Put as u32, 1, 0, 0, b"a").unwrap();
1186 wal.sync().unwrap();
1187 wal.next_lsn()
1188 };
1189
1190 let mut bytes = vec![0u8; 64 * 1024];
1194 bytes[0..4].copy_from_slice(&crate::record::WAL_MAGIC.to_le_bytes());
1195 std::fs::write(segment_path(dir.path(), next), &bytes).unwrap();
1196
1197 let mut wal = SegmentedWal::open(test_config(dir.path()))
1198 .expect("a torn newest segment is not a format this build cannot read");
1199 let lsn = wal.append(RecordType::Put as u32, 1, 0, 0, b"b").unwrap();
1200 assert_eq!(
1201 lsn, next,
1202 "the writer has to keep working over the torn tail, not just open"
1203 );
1204 wal.sync().unwrap();
1205 drop(wal);
1206 let (records, _) = replay_from_limit_dir(dir.path(), 0, usize::MAX, None)
1207 .expect("the resumed log replays without a gap");
1208 assert_eq!(records.len(), 2, "both records replay");
1209 }
1210}