use kevy_resp::{ArgvView, encode_array_len, encode_error, encode_integer, encode_simple_string};
use kevy_rt::Route;
use crate::ops::wrong_args;
use crate::state::ReplicationState;
const REPL_WAIT_DEFAULT_TIMEOUT_MS: u64 = 1_000;
pub(crate) fn wait_route<A: ArgvView + ?Sized>(repl: &ReplicationState, args: &A) -> Route {
if repl.is_replica() {
return Route::Local;
}
match parse_wait_args(args) {
Some((numreplicas, timeout_ms)) => Route::ReplWait { numreplicas, timeout_ms },
None => Route::Local,
}
}
pub(crate) fn token_route<A: ArgvView + ?Sized>(repl: &ReplicationState, args: &A) -> Route {
if args.len() == 1 && !repl.is_replica() {
return Route::ReplToken;
}
Route::Local
}
pub(crate) fn repl_wait_route<A: ArgvView + ?Sized>(repl: &ReplicationState, args: &A) -> Route {
if !repl.is_replica() {
return Route::Local;
}
let Some(tok) = parse_repl_wait(args) else {
return Route::Local;
};
if !gens_match(repl, &tok) {
return Route::Local; }
Route::ReplBarrier {
offsets: tok.pairs.iter().map(|(_, off)| *off).collect(),
timeout_ms: tok.timeout_ms,
miss: misdirected_reply(repl),
}
}
pub(crate) fn cmd_wait<A: ArgvView + ?Sized>(repl: &ReplicationState, args: &A, out: &mut Vec<u8>) {
if args.len() != 3 {
return wrong_args(out, "wait");
}
if repl.is_replica() {
return encode_error(out, "ERR WAIT cannot be used with replica instances");
}
if parse_wait_args(args).is_none() {
return encode_error(out, "ERR value is not an integer or out of range");
}
encode_integer(out, 0);
}
pub(crate) fn cmd_repl_token<A: ArgvView + ?Sized>(
repl: &ReplicationState,
args: &A,
out: &mut Vec<u8>,
) {
if args.len() != 1 {
return wrong_args(out, "repl.token");
}
if !repl.is_replica() {
return encode_error(out, "ERR REPL.TOKEN needs the kevy server runtime on a primary");
}
let gens = repl.upstream_gens();
let offs = repl.applied_runner_offsets();
encode_array_len(out, (gens.len() * 2) as i64);
for (g, off) in gens.iter().zip(offs.iter()) {
encode_integer(out, *g as i64);
encode_integer(out, *off as i64);
}
}
pub(crate) fn cmd_repl_wait<A: ArgvView + ?Sized>(
repl: &ReplicationState,
args: &A,
out: &mut Vec<u8>,
) {
let Some(tok) = parse_repl_wait(args) else {
return encode_error(
out,
"ERR REPL.WAIT g0 off0 [g1 off1 ...] [TIMEOUT ms] — pass a token from REPL.TOKEN",
);
};
if !repl.is_replica() {
return encode_simple_string(out, "OK");
}
let gens = repl.upstream_gens();
if gens.len() != tok.pairs.len() {
return encode_error(
out,
&format!(
"ERR REPL.WAIT token has {} (gen, offset) pair(s) but this replica follows {} \
stream(s); take the token from this server's primary with REPL.TOKEN",
tok.pairs.len(),
gens.len(),
),
);
}
out.extend_from_slice(&misdirected_reply(repl));
}
pub(crate) fn parse_wait_args<A: ArgvView + ?Sized>(args: &A) -> Option<(u32, u64)> {
if args.len() != 3 {
return None;
}
let need: u32 = parse_u64(&args[1])?.try_into().ok()?;
let timeout_ms = parse_u64(&args[2])?;
Some((need, timeout_ms))
}
pub(crate) struct ReplWaitToken {
pub(crate) pairs: Vec<(u64, u64)>,
pub(crate) timeout_ms: u64,
}
pub(crate) fn parse_repl_wait<A: ArgvView + ?Sized>(args: &A) -> Option<ReplWaitToken> {
let mut end = args.len();
let mut timeout_ms = REPL_WAIT_DEFAULT_TIMEOUT_MS;
if end >= 3 && args[end - 2].eq_ignore_ascii_case(b"TIMEOUT") {
timeout_ms = parse_u64(&args[end - 1])?;
end -= 2;
}
let n = end.checked_sub(1)?; if n == 0 || !n.is_multiple_of(2) {
return None;
}
let mut pairs = Vec::with_capacity(n / 2);
let mut i = 1;
while i < end {
let g = parse_u64(&args[i])?;
let off = parse_u64(&args[i + 1])?;
pairs.push((g, off));
i += 2;
}
Some(ReplWaitToken { pairs, timeout_ms })
}
fn gens_match(repl: &ReplicationState, tok: &ReplWaitToken) -> bool {
let gens = repl.upstream_gens();
gens.len() == tok.pairs.len()
&& tok.pairs.iter().zip(gens.iter()).all(|((g, _), known)| *g == *known && *g != 0)
}
fn misdirected_reply(repl: &ReplicationState) -> Vec<u8> {
match repl.current_upstream() {
Some((host, port)) => format!("-MISDIRECTED writer is {host}:{port}\r\n").into_bytes(),
None => b"-MISDIRECTED writer is unknown\r\n".to_vec(),
}
}
fn parse_u64(b: &[u8]) -> Option<u64> {
std::str::from_utf8(b).ok()?.parse::<u64>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use kevy_resp::Argv;
fn argv(parts: &[&str]) -> Argv {
let mut a = Argv::default();
for p in parts {
a.push(p.as_bytes());
}
a
}
#[test]
fn parse_wait_args_accepts_valid_and_rejects_garbage() {
assert_eq!(parse_wait_args(&argv(&["WAIT", "1", "500"])), Some((1, 500)));
assert_eq!(parse_wait_args(&argv(&["WAIT", "0", "0"])), Some((0, 0)));
assert_eq!(parse_wait_args(&argv(&["WAIT", "-1", "500"])), None);
assert_eq!(parse_wait_args(&argv(&["WAIT", "1", "-5"])), None);
assert_eq!(parse_wait_args(&argv(&["WAIT", "x", "500"])), None);
assert_eq!(parse_wait_args(&argv(&["WAIT", "1"])), None);
}
#[test]
fn parse_repl_wait_pairs_and_default_timeout() {
let t = parse_repl_wait(&argv(&["REPL.WAIT", "1", "42"])).unwrap();
assert_eq!(t.pairs, vec![(1, 42)]);
assert_eq!(t.timeout_ms, REPL_WAIT_DEFAULT_TIMEOUT_MS);
}
#[test]
fn parse_repl_wait_multi_pair_with_timeout_clause() {
let t =
parse_repl_wait(&argv(&["REPL.WAIT", "1", "10", "1", "20", "TIMEOUT", "250"])).unwrap();
assert_eq!(t.pairs, vec![(1, 10), (1, 20)]);
assert_eq!(t.timeout_ms, 250);
}
#[test]
fn parse_repl_wait_rejects_malformed() {
assert!(parse_repl_wait(&argv(&["REPL.WAIT"])).is_none());
assert!(parse_repl_wait(&argv(&["REPL.WAIT", "1", "10", "2"])).is_none());
assert!(parse_repl_wait(&argv(&["REPL.WAIT", "1", "x"])).is_none());
assert!(parse_repl_wait(&argv(&["REPL.WAIT", "1", "10", "TIMEOUT"])).is_none());
assert!(parse_repl_wait(&argv(&["REPL.WAIT", "1", "10", "TIMEOUT", "x"])).is_none());
}
fn primary() -> ReplicationState {
ReplicationState::new(1, false, 0)
}
#[test]
fn wait_route_shapes() {
let repl = primary();
assert!(matches!(
wait_route(&repl, &argv(&["WAIT", "1", "500"])),
Route::ReplWait { numreplicas: 1, timeout_ms: 500 }
));
assert!(matches!(wait_route(&repl, &argv(&["WAIT", "x", "500"])), Route::Local));
assert!(matches!(wait_route(&repl, &argv(&["WAIT"])), Route::Local));
}
#[test]
fn repl_token_route_is_fanout_on_primary_and_local_on_arity_error() {
let repl = primary();
assert!(matches!(token_route(&repl, &argv(&["REPL.TOKEN"])), Route::ReplToken));
assert!(matches!(token_route(&repl, &argv(&["REPL.TOKEN", "x"])), Route::Local));
}
#[test]
fn repl_wait_on_primary_replies_ok_immediately() {
let repl = primary();
assert!(matches!(repl_wait_route(&repl, &argv(&["REPL.WAIT", "1", "10"])), Route::Local));
let mut out = Vec::new();
cmd_repl_wait(&repl, &argv(&["REPL.WAIT", "1", "10"]), &mut out);
assert_eq!(out, b"+OK\r\n");
}
#[test]
fn repl_wait_malformed_token_errors() {
let mut out = Vec::new();
cmd_repl_wait(&primary(), &argv(&["REPL.WAIT", "nope"]), &mut out);
assert!(out.starts_with(b"-ERR REPL.WAIT"), "{}", String::from_utf8_lossy(&out));
}
#[test]
fn wait_on_replica_is_rejected() {
let repl = primary();
repl.force_replica_flag();
assert!(matches!(wait_route(&repl, &argv(&["WAIT", "1", "500"])), Route::Local));
let mut out = Vec::new();
cmd_wait(&repl, &argv(&["WAIT", "1", "500"]), &mut out);
assert!(
out.starts_with(b"-ERR WAIT cannot be used with replica"),
"{}",
String::from_utf8_lossy(&out)
);
}
}