use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use crate::replay_walk::{ReplayStop, walk_v2};
use kevy_resp::Argv;
pub fn replay_aof<F: FnMut(Argv)>(path: &Path, mut apply: F) -> io::Result<ReplayReport> {
if matches!(sniff_format(path)?, crate::AofFormat::V2) {
return stream_v2(path, Some(&mut apply), false, false);
}
let mut data = Vec::new();
match File::open(path) {
Ok(mut f) => {
f.read_to_end(&mut data)?;
}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(ReplayReport::default()),
Err(e) => return Err(e),
}
replay_v1_slice(path, &data, &mut apply, false)
}
pub fn replay_aof_quiet<F: FnMut(Argv)>(
path: &Path,
resync: bool,
mut apply: F,
) -> io::Result<ReplayReport> {
if matches!(sniff_format(path)?, crate::AofFormat::V2) {
return stream_v2(path, Some(&mut apply), resync, true);
}
let mut data = Vec::new();
match File::open(path) {
Ok(mut f) => {
f.read_to_end(&mut data)?;
}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(ReplayReport::default()),
Err(e) => return Err(e),
}
replay_v1_slice(path, &data, &mut apply, true)
}
fn v1_walk<F: FnMut(Argv)>(data: &[u8], pos: &mut usize, apply: &mut F) -> (ReplayStop, u64) {
let total = data.len();
let mut replayed: u64 = 0;
let stop = loop {
if *pos >= total {
break ReplayStop::Clean;
}
match kevy_resp::parse_command(&data[*pos..]) {
Ok(Some((args, consumed))) => {
apply(args);
*pos += consumed;
replayed += 1;
}
Ok(None) => break ReplayStop::TruncatedTail,
Err(e) => break ReplayStop::CorruptFrame(format!("{e:?}")),
}
};
(stop, replayed)
}
fn replay_v1_slice<F: FnMut(Argv)>(
path: &Path,
data: &[u8],
apply: &mut F,
quiet_info: bool,
) -> io::Result<ReplayReport> {
let total = data.len();
if total == 0 {
return Ok(ReplayReport::default());
}
let start = std::time::Instant::now();
let mut pos =
if data.starts_with(crate::aof::AOF_MAGIC) { crate::aof::AOF_MAGIC.len() } else { 0 };
let (stop, replayed) = v1_walk(data, &mut pos, apply);
let elapsed_ms = start.elapsed().as_millis();
let corrupt = matches!(stop, ReplayStop::CorruptFrame(_));
if corrupt || !quiet_info {
log_replay_summary(path, total, pos, replayed, &data[pos.min(total)..], stop, elapsed_ms);
}
Ok(ReplayReport {
commands: replayed,
bytes: total as u64,
replayed_bytes: pos as u64,
dropped_bytes: (total - pos) as u64,
corrupt,
resynced_ranges: Vec::new(),
})
}
pub fn replay_aof_resync<F: FnMut(Argv)>(path: &Path, mut apply: F) -> io::Result<ReplayReport> {
if matches!(sniff_format(path)?, crate::AofFormat::V2) {
return stream_v2(path, Some(&mut apply), true, false);
}
replay_aof(path, apply)
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ReplayReport {
pub commands: u64,
pub bytes: u64,
pub replayed_bytes: u64,
pub dropped_bytes: u64,
pub corrupt: bool,
pub resynced_ranges: Vec<(u64, u64)>,
}
pub(crate) fn sniff_format(path: &Path) -> io::Result<crate::AofFormat> {
let mut head = [0u8; 9];
match File::open(path) {
Ok(mut f) => match f.read_exact(&mut head) {
Ok(()) if head == *crate::record::AOF2_MAGIC => Ok(crate::AofFormat::V2),
_ => Ok(crate::AofFormat::V1),
},
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(crate::AofFormat::V1),
Err(e) => Err(e),
}
}
fn stream_v2(
path: &Path,
mut apply: Option<&mut dyn FnMut(Argv)>,
resync: bool,
quiet_info: bool,
) -> io::Result<ReplayReport> {
use std::io::BufReader;
let file = match File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(ReplayReport::default()),
Err(e) => return Err(e),
};
let total = file.metadata().map_or(0, |m| m.len());
let mut r = BufReader::with_capacity(256 * 1024, file);
let mut magic = [0u8; 9];
r.read_exact(&mut magic)?; let start = std::time::Instant::now();
let mut w = walk_v2(&mut r, magic.len() as u64, &mut apply)?;
let corrupt = matches!(w.stop, ReplayStop::CorruptFrame(_));
let mut ranges: Vec<(u64, u64)> = Vec::new();
if resync && !matches!(w.stop, ReplayStop::Clean) {
crate::replay_resync::resync_fallback(path, &mut w, &mut apply, &mut ranges)?;
}
let corrupt = corrupt || !ranges.is_empty();
let elapsed_ms = start.elapsed().as_millis();
if apply.is_some() && (corrupt || !quiet_info) {
log_replay_summary(
path,
total as usize,
w.pos as usize,
w.replayed,
&w.preview[..w.preview_len],
w.stop,
elapsed_ms,
);
}
Ok(ReplayReport {
commands: w.replayed,
bytes: total,
replayed_bytes: w.pos,
dropped_bytes: total.saturating_sub(w.pos),
corrupt,
resynced_ranges: ranges,
})
}
pub(crate) fn valid_prefix_len_of_file(path: &Path, resync: bool) -> io::Result<u64> {
if matches!(sniff_format(path)?, crate::AofFormat::V2) {
return Ok(stream_v2(path, None, resync, false)?.replayed_bytes);
}
let mut data = Vec::new();
match File::open(path) {
Ok(mut f) => {
f.read_to_end(&mut data)?;
}
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(0),
Err(e) => return Err(e),
}
Ok(valid_prefix_len(&data) as u64)
}
fn valid_prefix_len(data: &[u8]) -> usize {
let total = data.len();
let is_v2 = data.starts_with(crate::record::AOF2_MAGIC);
let mut pos = if is_v2 || data.starts_with(crate::aof::AOF_MAGIC) {
crate::record::AOF2_MAGIC.len()
} else {
0
};
loop {
if pos >= total {
break;
}
if is_v2 {
match crate::record::next_record(data, pos) {
crate::record::RecordStep::Ok { payload, consumed } => {
match kevy_resp::parse_command(payload) {
Ok(Some((_, used))) if used == payload.len() => pos += consumed,
_ => break,
}
}
_ => break,
}
continue;
}
match kevy_resp::parse_command(&data[pos..]) {
Ok(Some((_, consumed))) => pos += consumed,
Ok(None) | Err(_) => break,
}
}
pos
}
use crate::replay_log::log_replay_summary;