use std::path::Path;
use crate::crypto::KeyRing;
use crate::error::{Result, WalError};
use crate::record::WalRecord;
use crate::segment::{SegmentContinuity, SegmentMeta, check_retained_floor};
use super::reader::MmapWalReader;
const PARALLEL_SEGMENT_THRESHOLD: usize = 4;
struct SegmentScan {
records: Vec<WalRecord>,
last_lsn: u64,
}
pub fn replay_segments_mmap(
wal_dir: &Path,
from_lsn: u64,
keys: Option<&KeyRing>,
) -> Result<Vec<WalRecord>> {
let segments = crate::segment::discover_segments(wal_dir)?;
check_retained_floor(&segments, from_lsn)?;
let live = filter_segments_by_lsn(&segments, from_lsn);
if live.len() < PARALLEL_SEGMENT_THRESHOLD {
return replay_segments_sequential(live, from_lsn, keys);
}
replay_segments_parallel(live, from_lsn, keys)
}
fn filter_segments_by_lsn(segments: &[SegmentMeta], from_lsn: u64) -> &[SegmentMeta] {
let mut start = 0;
for i in 0..segments.len() {
let upper = segments.get(i + 1).map(|s| s.first_lsn).unwrap_or(u64::MAX);
if upper > from_lsn {
start = i;
break;
}
start = i + 1;
}
if start >= segments.len() {
return &[];
}
&segments[start..]
}
fn scan_segment(
segment: &SegmentMeta,
from_lsn: u64,
keys: Option<&KeyRing>,
) -> Result<SegmentScan> {
let mut reader = MmapWalReader::open(&segment.path, keys)?;
let mut records = Vec::new();
let mut last_lsn = 0u64;
while let Some(record) = reader
.next_record()
.map_err(|error| error.with_segment_path(&segment.path))?
{
last_lsn = record.header.lsn;
if record.header.lsn >= from_lsn {
records.push(record);
}
}
crate::torn_tail::verify_committed_prefix(&segment.path, reader.stop_reason(), last_lsn)?;
reader.release_pages();
Ok(SegmentScan { records, last_lsn })
}
fn replay_segments_sequential(
segments: &[SegmentMeta],
from_lsn: u64,
keys: Option<&KeyRing>,
) -> Result<Vec<WalRecord>> {
let mut records = Vec::new();
let mut continuity = SegmentContinuity::new();
for seg in segments {
continuity.check(seg)?;
let scan = scan_segment(seg, from_lsn, keys)?;
records.extend(scan.records);
continuity.completed(seg, scan.last_lsn);
}
Ok(records)
}
fn replay_segments_parallel(
segments: &[SegmentMeta],
from_lsn: u64,
keys: Option<&KeyRing>,
) -> Result<Vec<WalRecord>> {
let mut per_segment: Vec<Result<SegmentScan>> = Vec::with_capacity(segments.len());
std::thread::scope(|scope| {
let handles: Vec<_> = segments
.iter()
.map(|seg| scope.spawn(move || scan_segment(seg, from_lsn, keys)))
.collect();
for handle in handles {
per_segment.push(handle.join().unwrap_or_else(|_| {
Err(WalError::Io(std::io::Error::other(
"segment replay thread panicked",
)))
}));
}
});
let total_estimate: usize = per_segment
.iter()
.map(|r| r.as_ref().map(|scan| scan.records.len()).unwrap_or(0))
.sum();
let mut records = Vec::with_capacity(total_estimate);
let mut continuity = SegmentContinuity::new();
for (seg, seg_result) in segments.iter().zip(per_segment) {
continuity.check(seg)?;
let scan = seg_result?;
records.extend(scan.records);
continuity.completed(seg, scan.last_lsn);
}
Ok(records)
}
pub fn replay_segments_mmap_limit(
wal_dir: &Path,
from_lsn: u64,
max_records: usize,
keys: Option<&KeyRing>,
) -> Result<(Vec<WalRecord>, bool)> {
let segments = crate::segment::discover_segments(wal_dir)?;
check_retained_floor(&segments, from_lsn)?;
let live = filter_segments_by_lsn(&segments, from_lsn);
let mut records = Vec::with_capacity(max_records.min(4096));
let mut continuity = SegmentContinuity::new();
for seg in live {
continuity.check(seg)?;
let mut reader = MmapWalReader::open(&seg.path, keys)?;
let mut last_lsn = 0u64;
while let Some(record) = reader
.next_record()
.map_err(|error| error.with_segment_path(&seg.path))?
{
last_lsn = record.header.lsn;
if record.header.lsn >= from_lsn {
records.push(record);
if records.len() >= max_records {
return Ok((records, true));
}
}
}
crate::torn_tail::verify_committed_prefix(&seg.path, reader.stop_reason(), last_lsn)?;
reader.release_pages();
continuity.completed(seg, last_lsn);
}
Ok((records, false))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::RecordType;
#[test]
fn replay_mmap_from_lsn() {
let dir = tempfile::tempdir().unwrap();
let wal_dir = dir.path().join("wal");
std::fs::create_dir_all(&wal_dir).unwrap();
let config = crate::segmented::SegmentedWalConfig::for_testing(wal_dir.clone());
let mut wal = crate::segmented::SegmentedWal::open(config).unwrap();
let lsn1 = wal.append(RecordType::Put as u32, 1, 0, 0, b"a").unwrap();
let lsn2 = wal.append(RecordType::Put as u32, 1, 0, 0, b"b").unwrap();
let lsn3 = wal.append(RecordType::Put as u32, 1, 0, 0, b"c").unwrap();
wal.sync().unwrap();
let records = replay_segments_mmap(&wal_dir, lsn2, None).unwrap();
assert_eq!(records.len(), 2);
assert_eq!(records[0].header.lsn, lsn2);
assert_eq!(records[1].header.lsn, lsn3);
let all = replay_segments_mmap(&wal_dir, lsn1, None).unwrap();
assert_eq!(all.len(), 3);
}
#[test]
fn replay_mmap_below_the_retained_floor_is_rejected() {
use crate::error::WalError;
let dir = tempfile::tempdir().unwrap();
let wal_dir = dir.path().join("wal");
let config = crate::segmented::SegmentedWalConfig {
wal_dir: wal_dir.clone(),
segment_target_size: 100,
writer_config: crate::writer::WalWriterConfig {
use_direct_io: false,
..Default::default()
},
};
let mut wal = crate::segmented::SegmentedWal::open(config).unwrap();
for i in 0..40u32 {
wal.append(
RecordType::Put as u32,
1,
0,
0,
format!("record-{i:04}").as_bytes(),
)
.unwrap();
wal.sync().unwrap();
}
assert!(wal.truncate_before(20).unwrap().segments_deleted > 0);
let floor = wal.list_segments().unwrap()[0].first_lsn;
assert!(floor > 1);
let records = replay_segments_mmap(&wal_dir, floor, None).expect("floor is retained");
assert_eq!(records[0].header.lsn, floor);
match replay_segments_mmap(&wal_dir, floor - 1, None) {
Err(WalError::ReplayBelowRetainedFloor {
from_lsn,
retained_floor_lsn,
..
}) => {
assert_eq!(from_lsn, floor - 1);
assert_eq!(retained_floor_lsn, floor);
}
other => panic!("expected a retained-floor violation, got {other:?}"),
}
assert!(matches!(
replay_segments_mmap_limit(&wal_dir, 1, 5, None),
Err(WalError::ReplayBelowRetainedFloor { .. })
));
}
}