use std::net::{IpAddr, Ipv4Addr};
use kevy_config::{Config, ReplicationRole};
use kevy_rt::{Commands, Runtime, replica_inbox_pair};
use crate::replica_state;
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,
nshards: usize,
) -> Runtime<C> {
let mut senders = Vec::with_capacity(nshards);
let mut receivers = Vec::with_capacity(nshards);
for _ in 0..nshards {
let (tx, rx) = replica_inbox_pair();
senders.push(tx);
receivers.push(rx);
}
replica_state::install_senders(senders);
let runtime = runtime.with_replica_inboxes(receivers);
match cfg.replication.role {
ReplicationRole::Primary => 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 => {
spawn_initial_runners_from_config(cfg);
runtime
}
ReplicationRole::Standalone => runtime,
}
}
fn spawn_initial_runners_from_config(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) = replica_state::start_runners((host, port_base)) {
eprintln!("kevy: start_runners failed: {e}");
}
}
pub(crate) fn retarget_upstream(upstream: &str) -> Result<(), &'static str> {
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")?;
replica_state::start_runners((host, port_base))
}
pub(crate) fn demote_to_standalone() {
replica_state::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(_))));
}
}