use crate::stage_ring::StageRing;
use crate::tests::temp_file;
use crate::{Aof, Fsync, replay_aof};
use kevy_resp::Argv;
const CAP: u64 = 64 * 1024;
fn cmd(parts: &[&[u8]]) -> Argv {
Argv::from(parts.iter().map(|p| p.to_vec()).collect::<Vec<_>>())
}
fn set(i: usize, size: usize) -> Argv {
cmd(&[b"SET", format!("k{i}").as_bytes(), &vec![b'v'; size]])
}
fn words(a: &Argv) -> Vec<Vec<u8>> {
(0..a.len()).map(|i| a[i].to_vec()).collect()
}
fn replayed(path: &std::path::Path) -> Vec<Vec<Vec<u8>>> {
let mut out = Vec::new();
replay_aof(path, |a| out.push(words(&a))).unwrap();
out
}
pub(crate) fn reopen(
aof: &std::path::Path,
ring: &std::path::Path,
) -> (Aof, crate::StageOpen, Vec<Argv>) {
let mut log = Aof::open(aof, Fsync::EverySec).unwrap();
let mut got = Vec::new();
let found = log.open_stage(ring, CAP, |a| got.push(std::mem::take(a))).unwrap();
(log, found, got)
}
fn run(log: &mut Aof) {
for i in 0..40 {
log.append(&set(i, 300)).unwrap();
if i % 7 == 0 {
let _ = log.tick().unwrap();
}
}
log.begin_group();
log.append(&set(100, 10)).unwrap();
log.append(&set(101, 10)).unwrap();
log.end_group().unwrap();
log.truncate().unwrap();
for i in 200..260 {
log.append(&set(i, 2000)).unwrap();
}
log.sync_now().unwrap();
}
#[test]
fn a_staged_log_writes_the_same_bytes_as_a_plain_one() {
let (plain, staged) = (temp_file("stage-aof-plain"), temp_file("stage-aof-staged"));
let mut p = Aof::open(&plain, Fsync::EverySec).unwrap();
run(&mut p);
let (mut s, found, _) = reopen(&staged, &temp_file("stage-aof-ring"));
assert_eq!(found, crate::StageOpen::default());
assert!(s.stage_path().is_some());
run(&mut s);
assert_eq!(std::fs::read(&plain).unwrap(), std::fs::read(&staged).unwrap());
}
#[test]
fn a_killed_process_keeps_every_staged_append() {
let (aof, ring) = (temp_file("stage-kill-aof"), temp_file("stage-kill-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
log.append(&set(0, 50)).unwrap();
let _ = log.tick().unwrap(); for i in 1..6 {
log.append(&set(i, 50)).unwrap(); }
std::mem::forget(log);
assert_eq!(replayed(&aof).len(), 1, "only the drained record reached the file");
let (log, found, got) = reopen(&aof, &ring);
assert_eq!(found.recovered, 5);
assert_eq!(
got.iter().map(words).collect::<Vec<_>>(),
(1..6).map(|i| words(&set(i, 50))).collect::<Vec<_>>()
);
drop(log);
assert_eq!(replayed(&aof).len(), 6, "the recovered records were appended to the file");
let (_, again, got) = reopen(&aof, &ring);
assert_eq!((again.recovered, got.len()), (0, 0));
}
#[test]
fn a_copied_directory_keeps_every_staged_append() {
let (aof, ring) = (temp_file("stage-copy-aof"), temp_file("stage-copy-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
log.append(&set(0, 50)).unwrap();
let _ = log.tick().unwrap();
for i in 1..6 {
log.append(&set(i, 50)).unwrap();
}
std::mem::forget(log);
let (aof2, ring2) = (temp_file("stage-copy-aof2"), temp_file("stage-copy-ring2"));
std::fs::copy(&aof, &aof2).unwrap();
std::fs::copy(&ring, &ring2).unwrap();
let (_, found, got) = reopen(&aof2, &ring2);
assert_eq!((found.recovered, got.len(), found.discarded), (5, 5, None));
assert_eq!(replayed(&aof2).len(), 6);
}
#[test]
fn a_log_of_the_same_length_but_other_bytes_is_not_the_rings() {
let (aof, ring) = (temp_file("stage-other-aof"), temp_file("stage-other-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
log.append(&cmd(&[b"SET", b"k", b"vvvv"])).unwrap();
let _ = log.tick().unwrap();
log.append(&set(1, 50)).unwrap();
std::mem::forget(log);
let before = std::fs::metadata(&aof).unwrap().len();
std::fs::remove_file(&aof).unwrap();
let mut other = Aof::open(&aof, Fsync::EverySec).unwrap();
other.append(&cmd(&[b"SET", b"k", b"wwww"])).unwrap();
other.sync_now().unwrap();
drop(other);
assert_eq!(std::fs::metadata(&aof).unwrap().len(), before);
let (_, found, got) = reopen(&aof, &ring);
assert_eq!((found.recovered, got.len()), (0, 0));
assert!(found.discarded.is_some());
}
#[test]
fn a_kill_between_a_drain_and_its_header_is_not_replayed_twice() {
let (aof, ring) = (temp_file("stage-half-aof"), temp_file("stage-half-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
let before = StageRing::open_existing(&ring).unwrap().unwrap().1;
for i in 0..4 {
log.append(&set(i, 50)).unwrap();
}
let _ = log.tick().unwrap();
std::mem::forget(log);
let (mut r, _) = StageRing::open_existing(&ring).unwrap().unwrap();
r.mark_drained(before.drained, before.aof_len, before.log_id);
drop(r);
let (_, found, got) = reopen(&aof, &ring);
assert_eq!((found.recovered, got.len(), found.discarded), (0, 0, None));
assert_eq!(replayed(&aof).len(), 4);
}
#[test]
fn an_uncommitted_transaction_in_the_ring_is_dropped_and_leaves_the_log_whole() {
let (aof, ring) = (temp_file("stage-txn-aof"), temp_file("stage-txn-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
log.append(&set(0, 10)).unwrap();
log.begin_group();
log.append(&set(1, 10)).unwrap();
std::mem::forget(log);
let (mut log, found, got) = reopen(&aof, &ring);
assert_eq!(found.recovered, 1);
assert_eq!(got.iter().map(words).collect::<Vec<_>>(), [words(&set(0, 10))]);
log.append(&set(2, 10)).unwrap();
log.sync_now().unwrap();
drop(log);
assert_eq!(replayed(&aof), [words(&set(0, 10)), words(&set(2, 10))]);
}
#[test]
fn a_transaction_larger_than_the_ring_lands_whole_in_the_file() {
let (aof, ring) = (temp_file("stage-big-aof"), temp_file("stage-big-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
log.begin_group();
for i in 0..60 {
log.append(&set(i, 2000)).unwrap(); }
log.end_group().unwrap();
log.append(&set(99, 10)).unwrap();
std::mem::forget(log);
let (log, found, got) = reopen(&aof, &ring);
assert_eq!((found.recovered, got.len()), (1, 1), "only the record after the transaction");
drop(log);
let all = replayed(&aof);
assert_eq!(all.len(), 61);
assert_eq!(all[59], words(&set(59, 2000)));
}
#[test]
fn after_a_rewrite_the_ring_owes_only_what_came_after_it() {
let (aof, ring) = (temp_file("stage-rw-aof"), temp_file("stage-rw-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
let mut store = kevy_store::Store::new();
for i in 0..5 {
log.append(&set(i, 20)).unwrap();
store.set(
format!("k{i}").as_bytes(),
vec![b'v'; 20],
None,
kevy_store::SetCondition::Always,
);
}
log.rewrite_from(&store).unwrap();
log.append(&set(7, 20)).unwrap();
std::mem::forget(log);
let (_, found, got) = reopen(&aof, &ring);
assert_eq!(found.recovered, 1);
assert_eq!(got.iter().map(words).collect::<Vec<_>>(), [words(&set(7, 20))]);
}
#[test]
fn a_record_larger_than_the_ring_goes_to_the_file_and_survives_a_kill() {
let (aof, ring) = (temp_file("stage-huge-aof"), temp_file("stage-huge-ring"));
let (mut log, _, _) = reopen(&aof, &ring);
log.append(&set(0, 70 * 1024)).unwrap(); log.append(&set(1, 10)).unwrap(); std::mem::forget(log);
let (log, found, got) = reopen(&aof, &ring);
assert_eq!((found.recovered, found.discarded), (1, None));
assert_eq!(got.iter().map(words).collect::<Vec<_>>(), [words(&set(1, 10))]);
drop(log);
assert_eq!(replayed(&aof), [words(&set(0, 70 * 1024)), words(&set(1, 10))]);
}