use kevy_resp::CmdError;
use std::net::{IpAddr, Ipv4Addr};
use kevy_config::{Config, ReplicationRole};
use kevy_rt::{Commands, Runtime};
use crate::state::{ReplicationState, RuntimeState};
pub(crate) fn replication_port_base(cfg: &Config) -> u16 {
match cfg.replication.listen_port_base {
0 => cfg.server.port.saturating_add(10_000),
base => base,
}
}
pub(crate) fn apply<C: Commands>(
runtime: Runtime<C>,
cfg: &Config,
state: &RuntimeState,
) -> Runtime<C> {
let repl = &state.replication;
let receivers =
state.take_replica_inboxes().expect("replica inboxes are taken once, by this wiring");
let runtime = runtime.with_replica_inboxes(receivers);
match cfg.replication.role {
ReplicationRole::Primary => {
repl.set_min_replicas(cfg.replication.min_replicas_to_write);
runtime
.with_replication(true, cfg.replication.replication_buffer_size)
.with_replication_listener(replication_port_base(cfg))
.with_replication_reconnect_window(cfg.replication.reconnect_window_ms)
}
ReplicationRole::Replica => {
repl.set_read_only(cfg.replication.replica_read_only);
repl.set_max_staleness_ms(u64::from(cfg.replication.replica_max_staleness_ms));
spawn_initial_runners_from_config(repl, cfg);
runtime
.with_replication(true, cfg.replication.replication_buffer_size)
.with_replication_listener(replication_port_base(cfg))
.with_replication_reconnect_window(cfg.replication.reconnect_window_ms)
}
ReplicationRole::Standalone => runtime,
}
}
fn spawn_initial_runners_from_config(repl: &ReplicationState, cfg: &Config) {
let Some(upstream) = cfg.replication.upstream.as_deref() else {
eprintln!(
"kevy: [replication] role = \"replica\" but upstream is unset; \
no runners spawned — use REPLICAOF host port to set one"
);
return;
};
let Some((host_str, port_base)) = parse_upstream(upstream) else {
eprintln!(
"kevy: [replication] upstream {upstream:?} not parseable as host:port; \
no runners spawned"
);
return;
};
let Some(host) = resolve_host(&host_str) else {
eprintln!(
"kevy: [replication] upstream host {host_str:?} not resolvable; \
no runners spawned"
);
return;
};
if let Err(e) = repl.start_runners((host, port_base)) {
eprintln!("kevy: start_runners failed: {e}");
}
}
pub(crate) fn retarget_upstream(repl: &ReplicationState, upstream: &str) -> Result<(), CmdError> {
let (host_str, port_base) = parse_upstream(upstream).ok_or("upstream not host:port")?;
let host = resolve_host(&host_str).ok_or("upstream host not resolvable")?;
repl.start_runners((host, port_base))
}
pub(crate) fn demote_to_standalone(repl: &ReplicationState) {
repl.promote_stop_runners();
}
pub(crate) fn parse_upstream(s: &str) -> Option<(String, u16)> {
let idx = s.rfind(':')?;
let host = &s[..idx];
let port: u16 = s[idx + 1..].parse().ok()?;
if host.is_empty() {
return None;
}
Some((host.to_string(), port))
}
pub(crate) fn resolve_host(host: &str) -> Option<IpAddr> {
let host = host.strip_prefix('[').and_then(|s| s.strip_suffix(']')).unwrap_or(host);
if let Ok(ip) = host.parse::<IpAddr>() {
return Some(ip);
}
use std::net::ToSocketAddrs;
(host, 0u16)
.to_socket_addrs()
.ok()
.and_then(|mut it| it.next())
.map(|s| s.ip())
.or(Some(IpAddr::V4(Ipv4Addr::LOCALHOST)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_upstream_host_port() {
assert_eq!(parse_upstream("127.0.0.1:6004"), Some(("127.0.0.1".to_string(), 6004)));
}
#[test]
fn parse_upstream_missing_port_is_none() {
assert_eq!(parse_upstream("primary"), None);
}
#[test]
fn parse_upstream_empty_host_is_none() {
assert_eq!(parse_upstream(":6004"), None);
}
#[test]
fn parse_upstream_ipv6_brackets_kept_in_host() {
assert_eq!(parse_upstream("[::1]:7000"), Some(("[::1]".to_string(), 7000)));
}
#[test]
fn resolve_host_ipv4_literal() {
assert_eq!(resolve_host("10.0.0.1"), Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
}
#[test]
fn resolve_host_ipv6_bracketed_literal_strips() {
let got = resolve_host("[::1]");
assert!(matches!(got, Some(IpAddr::V6(_))));
}
}