use crate::SNAPSHOT_BUF_CAP;
use kevy_resp::{Argv, ArgvView};
use kevy_store::{StreamData, StreamId, Value};
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::Path;
pub fn dump_aof<S: crate::SnapshotSource>(path: &Path, src: &S) -> io::Result<(u64, u64)> {
let f = File::create(path)?;
let mut w = BufWriter::with_capacity(SNAPSHOT_BUF_CAP, f);
w.write_all(crate::aof::AOF_MAGIC)?;
let mut keys = 0u64;
let mut err: Option<io::Error> = None;
src.for_each_entry(|key, value, ttl_ms| {
if err.is_some() {
return;
}
if let Err(e) = write_value_as_commands(&mut w, key, value, ttl_ms) {
err = Some(e);
} else {
keys += 1;
}
});
if let Some(e) = err {
return Err(e);
}
let mut ferr: Option<io::Error> = None;
src.for_each_hash_ttl(|key, field, deadline_ms| {
if ferr.is_some() {
return;
}
let ms = deadline_ms.to_string();
let mut argv = kevy_resp::Argv::with_capacity(6, 0);
argv.push(b"HPEXPIREAT");
argv.push(key);
argv.push(ms.as_bytes());
argv.push(b"FIELDS");
argv.push(b"1");
argv.push(field);
if let Err(e) = write_multibulk(&mut w, &argv) {
ferr = Some(e);
}
});
if let Some(e) = ferr {
return Err(e);
}
w.flush()?;
let inner = w
.into_inner()
.map_err(|e| io::Error::other(e.to_string()))?;
let bytes = inner.metadata().map_or(0, |m| m.len());
inner.sync_all()?;
Ok((keys, bytes))
}
pub(crate) fn dump_store_to_buf<S: crate::SnapshotSource>(src: &S) -> (Vec<u8>, u64) {
let mut buf = Vec::with_capacity(crate::SNAPSHOT_BUF_CAP);
buf.extend_from_slice(crate::aof::AOF_MAGIC);
let mut keys = 0u64;
src.for_each_entry(|key, value, ttl_ms| {
let _ = write_value_as_commands(&mut buf, key, value, ttl_ms);
keys += 1;
});
(buf, keys)
}
#[cold]
fn write_value_as_commands<W: Write>(
w: &mut W,
key: &[u8],
value: &Value,
ttl_ms: Option<u64>,
) -> io::Result<()> {
match value {
Value::Str(s) => write_verb_items(w, b"SET", key, 1, [s.to_vec()])?,
Value::Int(n) => write_verb_items(w, b"SET", key, 1, [n.to_string().into_bytes()])?,
Value::ArcBulk(a) => write_verb_items(w, b"SET", key, 1, [a.as_ref().to_vec()])?,
Value::Hash(h) => {
let fv = h.iter().flat_map(|(f, v)| [f.to_vec(), v.clone()]);
write_verb_items(w, b"HSET", key, h.len() * 2, fv)?;
}
Value::SmallHashInline(h) => {
let fv = h.iter().flat_map(|(f, v)| [f.to_vec(), v.to_vec()]);
write_verb_items(w, b"HSET", key, h.len() * 2, fv)?;
}
Value::List(l) => write_verb_items(w, b"RPUSH", key, l.len(), l.iter().cloned())?,
Value::SmallListInline(l) => {
write_verb_items(w, b"RPUSH", key, l.len(), l.iter().map(<[u8]>::to_vec))?;
}
Value::Set(s) => {
write_verb_items(w, b"SADD", key, s.len(), s.iter().map(kevy_store::SmallBytes::to_vec))?;
}
Value::SmallSetInline(s) => {
write_verb_items(w, b"SADD", key, s.len(), s.iter().map(<[u8]>::to_vec))?;
}
Value::ZSet(z) => {
let ms = z.ordered().flat_map(|(m, sc)| [fmt_zset_score(sc), m.to_vec()]);
write_verb_items(w, b"ZADD", key, z.ordered().count() * 2, ms)?;
}
Value::SmallZSetInline(z) => {
let ms = z.iter().flat_map(|(m, sc)| [fmt_zset_score(sc), m.to_vec()]);
write_verb_items(w, b"ZADD", key, z.len() * 2, ms)?;
}
Value::Stream(s) => write_stream_as_commands(w, key, s)?,
}
write_pexpireat(w, key, ttl_ms)
}
fn write_pexpireat<W: Write>(w: &mut W, key: &[u8], ttl_ms: Option<u64>) -> io::Result<()> {
let Some(ms) = ttl_ms else { return Ok(()) };
let deadline = kevy_store::now_unix_ms().saturating_add(ms);
write_verb_items(w, b"PEXPIREAT", key, 1, [deadline.to_string().into_bytes()])
}
fn write_verb_items<W: Write>(
w: &mut W,
verb: &[u8],
key: &[u8],
expected: usize,
items: impl IntoIterator<Item = Vec<u8>>,
) -> io::Result<()> {
let mut argv: Vec<Vec<u8>> = Vec::with_capacity(2 + expected);
argv.push(verb.to_vec());
argv.push(key.to_vec());
argv.extend(items);
write_multibulk(w, &Argv::from(argv))
}
fn write_stream_as_commands<W: Write>(w: &mut W, key: &[u8], s: &StreamData) -> io::Result<()> {
for (id, fv) in s.iter_entries() {
let mut argv: Vec<Vec<u8>> = Vec::with_capacity(3 + fv.len() * 2);
argv.push(b"XADD".to_vec());
argv.push(key.to_vec());
argv.push(id.encode());
for (f, v) in fv {
argv.push(f.to_vec());
argv.push(v.to_vec());
}
write_multibulk(w, &Argv::from(argv))?;
}
let (len, last, mxd, added) =
(s.length(), s.last_id(), s.max_deleted_id(), s.entries_added());
if len == 0 && last != StreamId::MIN {
let argv = vec![
b"XADD".to_vec(), key.to_vec(), b"MAXLEN".to_vec(), b"0".to_vec(),
last.encode(), b"x".to_vec(), b"x".to_vec(),
];
write_multibulk(w, &Argv::from(argv))?;
}
let natural = if len > 0 {
(s.last_entry().map_or(StreamId::MIN, |(id, _)| id), len, StreamId::MIN)
} else {
(last, u64::from(last != StreamId::MIN), last)
};
if natural != (last, added, mxd) {
let argv = vec![
b"XSETID".to_vec(), key.to_vec(), last.encode(),
b"ENTRIESADDED".to_vec(), added.to_string().into_bytes(),
b"MAXDELETEDID".to_vec(), mxd.encode(),
];
write_multibulk(w, &Argv::from(argv))?;
}
write_stream_group_commands(w, key, s)
}
fn write_stream_group_commands<W: Write>(
w: &mut W,
key: &[u8],
s: &StreamData,
) -> io::Result<()> {
for g in s.export_groups() {
let last_delivered =
StreamId { ms: g.last_delivered.0, seq: g.last_delivered.1 };
let argv = vec![
b"XGROUP".to_vec(), b"CREATE".to_vec(), key.to_vec(), g.name.clone(),
last_delivered.encode(), b"MKSTREAM".to_vec(),
];
write_multibulk(w, &Argv::from(argv))?;
for (consumer, _last_seen_ms) in &g.consumers {
let argv = vec![
b"XGROUP".to_vec(), b"CREATECONSUMER".to_vec(), key.to_vec(),
g.name.clone(), consumer.clone(),
];
write_multibulk(w, &Argv::from(argv))?;
}
for (ms, seq, consumer, delivery_time_ms, delivery_count) in &g.pel {
let id = StreamId { ms: *ms, seq: *seq };
if !s.contains_entry(id) {
continue;
}
let argv = vec![
b"XCLAIM".to_vec(), key.to_vec(), g.name.clone(), consumer.clone(),
b"0".to_vec(), id.encode(),
b"TIME".to_vec(), delivery_time_ms.to_string().into_bytes(),
b"RETRYCOUNT".to_vec(), delivery_count.to_string().into_bytes(),
b"FORCE".to_vec(), b"JUSTID".to_vec(),
];
write_multibulk(w, &Argv::from(argv))?;
}
}
Ok(())
}
fn fmt_zset_score(s: f64) -> Vec<u8> {
#[allow(clippy::float_cmp)]
let is_integer_valued = s.is_finite() && s == s.trunc();
if is_integer_valued && s.abs() < 1e17 {
format!("{}", s as i64).into_bytes()
} else {
format!("{s:.17}").into_bytes()
}
}
pub(crate) fn estimate_multibulk_bytes<A: ArgvView + ?Sized>(args: &A) -> u64 {
let mut n: u64 = 3 + u64::from(decimal_digits(args.len() as u64));
for i in 0..args.len() {
let a = &args[i];
n += 3 + u64::from(decimal_digits(a.len() as u64)) + a.len() as u64 + 2;
}
n
}
#[inline]
fn decimal_digits(mut x: u64) -> u32 {
if x == 0 {
return 1;
}
let mut d = 0;
while x > 0 {
d += 1;
x /= 10;
}
d
}
pub(crate) fn write_multibulk<W: Write, A: ArgvView + ?Sized>(
w: &mut W,
args: &A,
) -> io::Result<()> {
write!(w, "*{}\r\n", args.len())?;
for i in 0..args.len() {
let a = &args[i];
write!(w, "${}\r\n", a.len())?;
w.write_all(a)?;
w.write_all(b"\r\n")?;
}
Ok(())
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) const REWRITE_EMIT_VERBS: &[&str] = &[
"SET", "HSET", "RPUSH", "SADD", "ZADD", "PEXPIREAT", "HPEXPIREAT",
"XADD", "XSETID", "XGROUP", "XCLAIM",
];
#[cfg(test)]
mod op_table_parity {
use super::REWRITE_EMIT_VERBS;
use kevy_resp::ops_table::{ops_with, surface};
use std::collections::BTreeSet;
#[test]
fn rewrite_manifest_matches_table() {
let m: BTreeSet<&str> = REWRITE_EMIT_VERBS.iter().copied().collect();
let t: BTreeSet<&str> = ops_with(surface::REWRITE).into_iter().collect();
assert_eq!(
m, t,
"rewrite emit set != OP_TABLE REWRITE flags — update both when changing rewrite_fmt"
);
}
#[test]
fn rewrite_manifest_verbs_have_source_literals() {
let src = include_str!("rewrite_fmt.rs");
for v in REWRITE_EMIT_VERBS {
let lit = format!("\"{v}");
assert!(
src.contains(&lit) || src.contains(&format!("b\"{v}\"")),
"REWRITE_EMIT_VERBS lists {v} but rewrite_fmt.rs has no literal for it"
);
}
}
}