use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
use crate::modes::{ReplayMode, ReplaySummary};
use crate::replay_walk::{ReplayStop, Sink, walk_v2};
use kevy_resp::Argv;
pub use crate::replay_report::ReplayReport;
pub fn replay_aof<F: FnMut(Argv)>(path: &Path, mut apply: F) -> io::Result<ReplayReport> {
replay_with(path, false, false, Sink::Owned(&mut apply))
}
pub fn replay_aof_in_place<F: FnMut(&mut Argv)>(
path: &Path,
mode: ReplayMode,
summary: ReplaySummary,
mut apply: F,
) -> io::Result<ReplayReport> {
let (resync, quiet) = (mode == ReplayMode::Resync, summary == ReplaySummary::Quiet);
replay_with(path, resync, quiet, Sink::InPlace(&mut apply))
}
fn replay_with(
path: &Path,
resync: bool,
quiet_info: bool,
mut sink: Sink<'_>,
) -> io::Result<ReplayReport> {
if matches!(sniff_format(path)?, crate::AofFormat::V2) {
return stream_v2(path, Some(sink), resync, quiet_info);
}
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 sink, quiet_info)
}
pub fn replay_aof_quiet<F: FnMut(Argv)>(
path: &Path,
mode: ReplayMode,
mut apply: F,
) -> io::Result<ReplayReport> {
replay_with(path, mode == ReplayMode::Resync, true, Sink::Owned(&mut apply))
}
fn v1_walk(data: &[u8], pos: &mut usize, sink: &mut Sink<'_>) -> (ReplayStop, u64) {
let total = data.len();
let mut replayed: u64 = 0;
let mut args = Argv::default();
let stop = loop {
if *pos >= total {
break ReplayStop::Clean;
}
match kevy_resp::parse_command_into(&data[*pos..], &mut args) {
Ok(Some(consumed)) => {
sink.deliver(&mut args);
*pos += consumed;
replayed += 1;
}
Ok(None) => break ReplayStop::TruncatedTail,
Err(e) => break ReplayStop::CorruptFrame(format!("{e:?}")),
}
};
(stop, replayed)
}
fn replay_v1_slice(
path: &Path,
data: &[u8],
sink: &mut Sink<'_>,
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, sink);
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,
zero_tail: 0,
corrupt,
resynced_ranges: Vec::new(),
})
}
pub fn replay_aof_resync<F: FnMut(Argv)>(path: &Path, mut apply: F) -> io::Result<ReplayReport> {
replay_with(path, true, false, Sink::Owned(&mut apply))
}
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<Sink<'_>>,
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 end = if ranges.is_empty() { w.settled_end() } else { w.pos };
if apply.is_some() {
log_v2_outcome(path, &w, total, end, corrupt, quiet_info, start.elapsed().as_millis());
}
Ok(ReplayReport {
commands: w.replayed,
bytes: total,
replayed_bytes: end,
dropped_bytes: total.saturating_sub(end).saturating_sub(w.zero_tail),
zero_tail: w.zero_tail,
corrupt,
resynced_ranges: ranges,
})
}
fn log_v2_outcome(
path: &Path,
w: &crate::replay_walk::V2Walk,
total: u64,
end: u64,
corrupt: bool,
quiet_info: bool,
elapsed_ms: u128,
) {
if !quiet_info && end < w.pos {
crate::replay_log::log_open_transaction(path, w.pos - end);
}
if corrupt || !quiet_info {
let preview = &w.preview[..w.preview_len];
log_replay_summary(
path,
total as usize,
w.pos as usize,
w.replayed,
preview,
w.stop.clone(),
elapsed_ms,
);
}
}
pub(crate) fn valid_prefix_len_of_file(path: &Path, resync: bool) -> io::Result<(u64, u64)> {
if matches!(sniff_format(path)?, crate::AofFormat::V2) {
let r = stream_v2(path, None, resync, false)?;
return Ok((r.replayed_bytes, r.zero_tail));
}
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, 0)),
Err(e) => return Err(e),
}
Ok((valid_prefix_len(&data) as u64, 0))
}
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;