use std::cell::RefCell;
use std::net::Ipv4Addr;
use kevy_config::ReplicationRole;
use kevy_resp::{ArgvView, encode_array_len, encode_bulk, encode_error, encode_integer, encode_simple_string};
use crate::config_global;
use super::wrong_args;
#[derive(Clone, Default)]
pub(crate) struct ReplicationView {
pub(crate) master_repl_offset: u64,
pub(crate) replicas: Vec<(Ipv4Addr, u16, u64)>,
}
thread_local! {
static REPLICATION_VIEW: RefCell<ReplicationView> = RefCell::new(ReplicationView::default());
}
pub(crate) fn set_replication_view(
master_repl_offset: u64,
replicas: Vec<(Ipv4Addr, u16, u64)>,
) {
REPLICATION_VIEW.with(|c| {
*c.borrow_mut() = ReplicationView {
master_repl_offset,
replicas,
};
});
}
pub(crate) fn replication_view() -> ReplicationView {
REPLICATION_VIEW.with(|c| c.borrow().clone())
}
pub(crate) fn cmd_role<A: ArgvView + ?Sized>(args: &A, out: &mut Vec<u8>) {
if args.len() != 1 {
return wrong_args(out, "role");
}
if let Some(snap) = crate::elect_integration::current_snapshot() {
use kevy_elect::message::Role as ElectRole;
match snap.role {
ElectRole::Primary => return emit_master(out),
ElectRole::Replica | ElectRole::Candidate => {
let (host, port) = match snap.current_primary.as_deref() {
Some(_addr_or_id) => current_primary_host_port_from_config(),
None => ("".to_string(), 0),
};
return emit_replica_addr(&host, port, out);
}
}
}
if let Some((host, port)) = crate::replica_state::current_upstream() {
let host_str = host.to_string();
return emit_replica_addr(&host_str, port, out);
}
let cfg = config_global::get();
match cfg.replication.role {
ReplicationRole::Standalone | ReplicationRole::Primary => emit_master(out),
ReplicationRole::Replica => emit_replica(cfg.replication.upstream.as_deref(), out),
}
}
fn current_primary_host_port_from_config() -> (String, u16) {
let snap = match crate::elect_integration::current_snapshot() {
Some(s) => s,
None => return (String::new(), 0),
};
let Some(pid) = snap.current_primary else {
return (String::new(), 0);
};
let cfg = config_global::get();
for p in &cfg.cluster.peers {
if p.node_id == pid {
return (p.host.clone(), p.port);
}
}
(String::new(), 0)
}
pub(crate) fn cmd_replicaof<A: ArgvView + ?Sized>(args: &A, out: &mut Vec<u8>) {
if args.len() != 3 {
return wrong_args(out, "replicaof");
}
let arg1 = &args[1];
let arg2 = &args[2];
if arg1.eq_ignore_ascii_case(b"NO") && arg2.eq_ignore_ascii_case(b"ONE") {
crate::replication::demote_to_standalone();
encode_simple_string(out, "OK");
return;
}
let Ok(port_str) = std::str::from_utf8(arg2) else {
return encode_error(out, "ERR value is not an integer or out of range");
};
let Ok(port): Result<u16, _> = port_str.parse() else {
return encode_error(out, "ERR value is not an integer or out of range");
};
let Ok(host_str) = std::str::from_utf8(arg1) else {
return encode_error(out, "ERR Invalid master host");
};
if host_str.is_empty() {
return encode_error(out, "ERR Invalid master host");
}
let upstream = format!("{host_str}:{port}");
if let Err(reason) = crate::replication::retarget_upstream(&upstream) {
return encode_error(out, &format!("ERR {reason}"));
}
encode_simple_string(out, "OK");
}
fn emit_master(out: &mut Vec<u8>) {
let view = replication_view();
encode_array_len(out, 3);
encode_bulk(out, b"master");
encode_integer(out, view.master_repl_offset as i64);
encode_array_len(out, view.replicas.len() as i64);
for (ip, port, offset) in &view.replicas {
let ip_str = ip.to_string();
let port_str = port.to_string();
let off_str = offset.to_string();
encode_array_len(out, 3);
encode_bulk(out, ip_str.as_bytes());
encode_bulk(out, port_str.as_bytes());
encode_bulk(out, off_str.as_bytes());
}
}
fn emit_replica(upstream: Option<&str>, out: &mut Vec<u8>) {
let (host, port) = parse_upstream(upstream);
emit_replica_addr(host, port, out);
}
fn emit_replica_addr(host: &str, port: u16, out: &mut Vec<u8>) {
encode_array_len(out, 5);
encode_bulk(out, b"slave");
encode_bulk(out, host.as_bytes());
encode_integer(out, i64::from(port));
encode_bulk(out, b"connect");
encode_integer(out, 0);
}
fn parse_upstream(s: Option<&str>) -> (&str, u16) {
let Some(s) = s else { return ("", 0) };
let (host, port_str) = match s.rfind(':') {
Some(idx) => (&s[..idx], &s[idx + 1..]),
None => return (s, 0),
};
let port: u16 = port_str.parse().unwrap_or(0);
(host, port)
}
#[cfg(test)]
mod tests {
use super::*;
use kevy_resp::Argv;
fn run(offset: u64, replica_count: usize) -> Vec<u8> {
let _g = crate::replica_state::TEST_STATE_GUARD.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
crate::replica_state::stop_runners();
let replicas: Vec<_> = (0..replica_count)
.map(|i| (Ipv4Addr::new(10, 0, 0, (i + 1) as u8), 6004, offset))
.collect();
set_replication_view(offset, replicas);
let mut a = Argv::default();
a.push(b"ROLE");
let mut out = Vec::new();
cmd_role(&a, &mut out);
out
}
#[test]
fn role_default_master_zero_offset() {
let out = run(0, 0);
assert_eq!(out, b"*3\r\n$6\r\nmaster\r\n:0\r\n*0\r\n");
}
#[test]
fn role_master_offset_reflects_view() {
let out = run(12345, 0);
assert_eq!(out, b"*3\r\n$6\r\nmaster\r\n:12345\r\n*0\r\n");
}
#[test]
fn role_master_emits_per_replica_array() {
let out = run(12345, 2);
let s = String::from_utf8(out).unwrap();
assert!(s.starts_with("*3\r\n$6\r\nmaster\r\n:12345\r\n"), "got: {s}");
assert!(s.contains("*2\r\n*3\r\n"), "expected inner *2 then *3 per entry; got: {s}");
assert!(s.contains("10.0.0.1"), "got: {s}");
assert!(s.contains("10.0.0.2"), "got: {s}");
}
#[test]
fn role_wrong_args_returns_error() {
let mut a = Argv::default();
a.push(b"ROLE");
a.push(b"extra");
let mut out = Vec::new();
cmd_role(&a, &mut out);
assert!(out.starts_with(b"-ERR"));
}
#[test]
fn parse_upstream_host_port() {
assert_eq!(parse_upstream(Some("127.0.0.1:6379")), ("127.0.0.1", 6379));
}
#[test]
fn parse_upstream_missing_port_defaults_to_zero() {
assert_eq!(parse_upstream(Some("primary.local")), ("primary.local", 0));
}
#[test]
fn parse_upstream_none_yields_empty() {
assert_eq!(parse_upstream(None), ("", 0));
}
#[test]
fn parse_upstream_ipv6_uses_rightmost_colon() {
assert_eq!(parse_upstream(Some("[::1]:7000")), ("[::1]", 7000));
}
fn replicaof(args: &[&[u8]]) -> Vec<u8> {
let _g = crate::replica_state::TEST_STATE_GUARD.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
crate::replica_state::stop_runners();
let mut a = Argv::default();
a.push(b"REPLICAOF");
for arg in args {
a.push(arg);
}
let mut out = Vec::new();
cmd_replicaof(&a, &mut out);
crate::replica_state::stop_runners();
out
}
#[test]
fn replicaof_host_port_returns_ok() {
let _g = crate::replica_state::TEST_STATE_GUARD.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
let (tx, _rx) = kevy_rt::replica_inbox_pair();
crate::replica_state::install_senders(vec![tx]);
drop(_g); assert_eq!(replicaof(&[b"127.0.0.1", b"6379"]), b"+OK\r\n");
}
#[test]
fn replicaof_no_one_returns_ok() {
assert_eq!(replicaof(&[b"NO", b"ONE"]), b"+OK\r\n");
assert_eq!(replicaof(&[b"no", b"one"]), b"+OK\r\n");
assert_eq!(replicaof(&[b"No", b"OnE"]), b"+OK\r\n");
}
#[test]
fn replicaof_wrong_args_errors() {
assert!(replicaof(&[]).starts_with(b"-ERR"));
assert!(replicaof(&[b"primary"]).starts_with(b"-ERR"));
assert!(replicaof(&[b"a", b"b", b"c"]).starts_with(b"-ERR"));
}
#[test]
fn replicaof_bad_port_errors() {
assert!(replicaof(&[b"primary", b"not-a-number"]).starts_with(b"-ERR"));
assert!(replicaof(&[b"primary", b"99999"]).starts_with(b"-ERR"));
assert!(replicaof(&[b"primary", b"-1"]).starts_with(b"-ERR"));
}
#[test]
fn replicaof_empty_host_errors() {
assert!(replicaof(&[b"", b"6379"]).starts_with(b"-ERR"));
}
}