use std::io::{self, Read, Write};
use kevy_store::Value;
use crate::snapshot_fmt::{OP_AUX, OP_GROUP_READS, read_bytes, read_u32, write_bytes};
use crate::{Argv, SnapshotSource};
#[derive(Debug, Clone, Copy)]
pub struct WithAux<'a, S> {
src: &'a S,
aux: Option<&'a Argv>,
}
impl<'a, S: SnapshotSource> WithAux<'a, S> {
pub fn new(src: &'a S, aux: Option<&'a Argv>) -> Self {
Self { src, aux }
}
}
impl<S: SnapshotSource> SnapshotSource for WithAux<'_, S> {
fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>)) {
self.src.for_each_entry(f);
}
fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
self.src.for_each_hash_ttl(f);
}
fn row_seg_files(&self) -> Vec<(u32, String)> {
self.src.row_seg_files()
}
fn aux_frame(&self) -> Option<Argv> {
self.aux.cloned()
}
}
pub(crate) fn write_aux<W: Write>(w: &mut W, frame: &Argv) -> io::Result<()> {
w.write_all(&[OP_AUX])?;
let n = u32::try_from(frame.len())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "aux frame too long"))?;
w.write_all(&n.to_le_bytes())?;
for i in 0..frame.len() {
write_bytes(w, &frame[i])?;
}
Ok(())
}
pub(crate) fn read_trailer<R: Read>(
r: &mut R,
store: &mut kevy_store::Store,
keep: &impl Fn(&[u8]) -> bool,
) -> io::Result<Option<Argv>> {
let mut aux = None;
loop {
let mut op = [0u8; 1];
match r.read_exact(&mut op) {
Ok(()) if op[0] == OP_AUX && aux.is_none() => aux = Some(read_aux(r)?),
Ok(()) if op[0] == OP_GROUP_READS => {
crate::snapshot_group_reads::read_and_apply(r, store, keep)?;
}
Ok(()) => return Ok(aux),
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(aux),
Err(e) => return Err(e),
}
}
}
fn read_aux<R: Read>(r: &mut R) -> io::Result<Argv> {
let n = read_u32(r)? as usize;
let mut parts = Vec::with_capacity(n.min(16));
for _ in 0..n {
parts.push(read_bytes(r)?);
}
Ok(Argv::from(parts))
}
#[cfg(test)]
mod tests {
use super::*;
use kevy_store::{SetCondition, Store};
fn frame() -> Argv {
Argv::from(vec![b"XINTERNAL.EXAMPLE".to_vec(), b"7".to_vec(), Vec::new(), vec![0, 255]])
}
fn store() -> Store {
let mut s = Store::new();
s.set(b"k", b"v".to_vec(), None, SetCondition::Always);
s.hset(b"h", &[(b"f", b"v")]).unwrap();
s.hexpire_at(b"h", &[b"f"], kevy_store::now_unix_ms() + 60_000, Default::default())
.unwrap();
s
}
#[test]
fn a_snapshot_carries_the_frame_after_the_keyspace() {
let (s, f) = (store(), frame());
let mut image = Vec::new();
crate::write_snapshot_to(&WithAux::new(&s, Some(&f)), &mut image).unwrap();
let mut bare = Vec::new();
crate::write_snapshot_to(&s, &mut bare).unwrap();
assert_eq!(image[..bare.len()], bare[..]);
assert_eq!(bare[bare.len() - 1], crate::snapshot_fmt::OP_EOF);
let mut back = Store::new();
let aux = crate::load_snapshot_with_aux(&mut back, image.as_slice(), |_| true).unwrap();
assert_eq!(aux, Some(f));
assert_eq!(back.dbsize(), 2);
let mut plain = Store::new();
crate::load_snapshot_from(&mut plain, image.as_slice()).unwrap();
assert_eq!(plain.dbsize(), 2);
}
#[test]
fn no_frame_changes_nothing() {
let s = store();
let (mut plain, mut wrapped) = (Vec::new(), Vec::new());
crate::write_snapshot_to(&s, &mut plain).unwrap();
crate::write_snapshot_to(&WithAux::new(&s, None), &mut wrapped).unwrap();
assert_eq!(plain, wrapped);
}
#[test]
fn a_rewritten_log_ends_with_the_frame() {
let (s, f) = (store(), frame());
let dir = kevy_tmpdir::unique_dir("aux-rewrite");
let path = dir.join("aof-0.aof");
crate::dump_aof(&path, &WithAux::new(&s, Some(&f))).unwrap();
let mut frames = Vec::new();
crate::replay_aof_quiet(&path, Default::default(), |a| frames.push(a)).unwrap();
assert_eq!(frames.last(), Some(&f));
let (image, _) =
crate::dump_store_to_buf(&WithAux::new(&s, Some(&f)), crate::AofFormat::V2);
let buf_path = dir.join("aof-1.aof");
std::fs::write(&buf_path, image).unwrap();
let mut from_buf = Vec::new();
crate::replay_aof_quiet(&buf_path, Default::default(), |a| from_buf.push(a)).unwrap();
assert_eq!(from_buf, frames);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn a_frame_that_does_not_fit_its_sink_fails_to_write() {
let f = frame();
let mut whole = Vec::new();
write_aux(&mut whole, &f).unwrap();
for room in [0, 1, 5] {
let mut buf = vec![0u8; room];
let err = write_aux(&mut buf.as_mut_slice(), &f).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WriteZero, "room {room}");
assert_eq!(buf[..], whole[..room]);
}
let big = Argv::from(vec![vec![b'x'; crate::snapshot_fmt::SNAPSHOT_BUF_CAP + 1]]);
let (mut sink, empty) = ([0u8; 64], Store::new());
let src = WithAux::new(&empty, Some(&big));
let err = crate::write_snapshot_to(&src, sink.as_mut_slice()).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
}
#[test]
fn what_follows_the_end_is_a_frame_nothing_or_an_error() {
let mut s = Store::new();
let all = |_: &[u8]| true;
assert_eq!(read_trailer(&mut &[0x42u8][..], &mut s, &all).unwrap(), None);
assert_eq!(read_trailer(&mut &[][..], &mut s, &all).unwrap(), None);
let cut_count = [OP_AUX, 1, 0];
let err = read_trailer(&mut &cut_count[..], &mut s, &all).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
let cut_part = [OP_AUX, 1, 0, 0, 0, 9, 0, 0, 0, b'x'];
let err = read_trailer(&mut &cut_part[..], &mut s, &all).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
let dir = kevy_tmpdir::TmpDir::new("aux-trailer-dir");
let mut not_a_file = std::fs::File::open(dir.path()).unwrap();
let err = read_trailer(&mut not_a_file, &mut s, &all).unwrap_err();
assert_ne!(err.kind(), io::ErrorKind::UnexpectedEof, "{err}");
}
}