use std::net::IpAddr;
use std::sync::Mutex;
use kevy_rt::ReplicaInboxSender;
use crate::replica_runner::ReplicaRunner;
static REPLICA_SENDERS: Mutex<Vec<ReplicaInboxSender>> = Mutex::new(Vec::new());
static REPLICA_RUNNERS: Mutex<Vec<ReplicaRunner>> = Mutex::new(Vec::new());
static REPLICA_UPSTREAM: Mutex<Option<(IpAddr, u16)>> = Mutex::new(None);
pub(crate) fn install_senders(senders: Vec<ReplicaInboxSender>) {
IS_REPLICA.store(false, std::sync::atomic::Ordering::Relaxed);
let mut guard = REPLICA_SENDERS.lock().expect("REPLICA_SENDERS poisoned");
*guard = senders;
}
pub(crate) fn senders_clone() -> Vec<ReplicaInboxSender> {
REPLICA_SENDERS
.lock()
.expect("REPLICA_SENDERS poisoned")
.clone()
}
pub(crate) fn stop_runners() {
IS_REPLICA.store(false, std::sync::atomic::Ordering::Relaxed);
let mut guard = REPLICA_RUNNERS.lock().expect("REPLICA_RUNNERS poisoned");
let runners = std::mem::take(&mut *guard);
drop(guard); for r in runners {
r.shutdown();
}
*REPLICA_UPSTREAM.lock().expect("REPLICA_UPSTREAM poisoned") = None;
APPLIED_RUNNER_OFFSETS
.lock()
.expect("APPLIED_RUNNER_OFFSETS poisoned")
.clear();
UPSTREAM_GENS.lock().expect("UPSTREAM_GENS poisoned").clear();
}
pub(crate) fn start_runners(upstream: (IpAddr, u16)) -> Result<(), &'static str> {
let senders = senders_clone();
if senders.is_empty() {
return Err("replica senders not installed (kevy::serve required)");
}
stop_runners();
let (host, port_base) = upstream;
let runner_count = if single_source() { 1 } else { senders.len() };
*APPLIED_RUNNER_OFFSETS
.lock()
.expect("APPLIED_RUNNER_OFFSETS poisoned") = vec![0; runner_count];
*UPSTREAM_GENS.lock().expect("UPSTREAM_GENS poisoned") = vec![0; runner_count];
let new_runners = if single_source() {
vec![ReplicaRunner::spawn_routed(
(host, port_base),
"kevy-replica-single".to_string(),
senders,
0,
)]
} else {
let mut fleet = Vec::with_capacity(senders.len());
for (shard_id, sender) in senders.into_iter().enumerate() {
let port = port_base.saturating_add(u16::try_from(shard_id).unwrap_or(u16::MAX));
let replica_id = format!("kevy-replica-{shard_id}");
fleet.push(ReplicaRunner::spawn((host, port), replica_id, sender, shard_id));
}
fleet
};
*REPLICA_RUNNERS.lock().expect("REPLICA_RUNNERS poisoned") = new_runners;
*REPLICA_UPSTREAM.lock().expect("REPLICA_UPSTREAM poisoned") = Some(upstream);
IS_REPLICA.store(true, std::sync::atomic::Ordering::Relaxed);
Ok(())
}
static SINGLE_SOURCE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static IS_REPLICA: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static READ_ONLY: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
pub(crate) fn is_replica() -> bool {
IS_REPLICA.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn force_replica_flag() {
IS_REPLICA.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn set_read_only(on: bool) {
READ_ONLY.store(on, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn read_only() -> bool {
READ_ONLY.load(std::sync::atomic::Ordering::Relaxed)
}
static PRIMARY_OFFSET: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static APPLIED_OFFSET: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static LAST_PING_MS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static APPLIED_RUNNER_OFFSETS: Mutex<Vec<u64>> = Mutex::new(Vec::new());
static UPSTREAM_GENS: Mutex<Vec<u64>> = Mutex::new(Vec::new());
pub(crate) fn upstream_gens() -> Vec<u64> {
UPSTREAM_GENS.lock().expect("UPSTREAM_GENS poisoned").clone()
}
pub(crate) fn applied_runner_offsets() -> Vec<u64> {
APPLIED_RUNNER_OFFSETS
.lock()
.expect("APPLIED_RUNNER_OFFSETS poisoned")
.clone()
}
static PROMOTION_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub(crate) fn promotion_epoch() -> u64 {
PROMOTION_EPOCH.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn promote_stop_runners() {
let was_replica = is_replica();
stop_runners();
if was_replica {
PROMOTION_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}
fn epoch_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub(crate) fn record_ping(runner_slot: usize, generation: u64, primary_offset: u64, applied: u64) {
PRIMARY_OFFSET.fetch_max(primary_offset, std::sync::atomic::Ordering::Relaxed);
APPLIED_OFFSET.fetch_max(applied, std::sync::atomic::Ordering::Relaxed);
LAST_PING_MS.store(epoch_ms(), std::sync::atomic::Ordering::Relaxed);
record_applied(runner_slot, applied);
let mut gens = UPSTREAM_GENS.lock().expect("UPSTREAM_GENS poisoned");
if let Some(slot) = gens.get_mut(runner_slot) {
*slot = generation;
}
}
pub(crate) fn record_applied(runner_slot: usize, applied: u64) {
let mut slots = APPLIED_RUNNER_OFFSETS
.lock()
.expect("APPLIED_RUNNER_OFFSETS poisoned");
if let Some(slot) = slots.get_mut(runner_slot) {
*slot = applied;
}
}
pub(crate) fn applied_offset_sum() -> u64 {
APPLIED_RUNNER_OFFSETS
.lock()
.expect("APPLIED_RUNNER_OFFSETS poisoned")
.iter()
.fold(0u64, |acc, v| acc.saturating_add(*v))
}
pub(crate) fn replica_link_view() -> (bool, u64, u64, u64) {
let last = LAST_PING_MS.load(std::sync::atomic::Ordering::Relaxed);
let now = epoch_ms();
let age_ms = now.saturating_sub(last);
let up = last != 0 && age_ms < 3_000;
let primary = PRIMARY_OFFSET.load(std::sync::atomic::Ordering::Relaxed);
let applied = APPLIED_OFFSET.load(std::sync::atomic::Ordering::Relaxed);
(up, applied, primary.saturating_sub(applied), age_ms / 1000)
}
static MIN_REPLICAS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
pub(crate) fn set_min_replicas(n: u32) {
MIN_REPLICAS.store(n, std::sync::atomic::Ordering::Relaxed);
}
static QUIESCE_TO: Mutex<Option<String>> = Mutex::new(None);
pub(crate) fn set_quiesce(target: Option<String>) {
*QUIESCE_TO.lock().expect("QUIESCE_TO poisoned") = target;
QUIESCED.store(
QUIESCE_TO.lock().expect("QUIESCE_TO poisoned").is_some(),
std::sync::atomic::Ordering::Relaxed,
);
}
static QUIESCED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static QUORUM_FENCED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
pub(crate) fn set_quorum_fence(on: bool) -> bool {
QUORUM_FENCED.swap(on, std::sync::atomic::Ordering::Relaxed) != on
}
static MAX_STALENESS_MS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub(crate) fn set_max_staleness_ms(v: u64) {
MAX_STALENESS_MS.store(v, std::sync::atomic::Ordering::Relaxed);
}
pub(crate) fn read_denied_reply() -> Option<Vec<u8>> {
let bound = MAX_STALENESS_MS.load(std::sync::atomic::Ordering::Relaxed);
if bound == 0 || !IS_REPLICA.load(std::sync::atomic::Ordering::Relaxed) {
return None;
}
let last = LAST_PING_MS.load(std::sync::atomic::Ordering::Relaxed);
if last != 0 && epoch_ms().saturating_sub(last) <= bound {
return None;
}
Some(
b"-STALE replica is stale; read the primary or raise replica_max_staleness_ms\r\n"
.to_vec(),
)
}
pub(crate) fn quiesce_active() -> bool {
QUIESCED.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn write_denied_reply() -> Option<Vec<u8>> {
if QUORUM_FENCED.load(std::sync::atomic::Ordering::Relaxed) {
return Some(
b"-NOREPLICAS primary lost quorum; writes fenced\r\n".to_vec(),
);
}
if QUIESCED.load(std::sync::atomic::Ordering::Relaxed) {
let g = QUIESCE_TO.lock().expect("QUIESCE_TO poisoned");
if let Some(t) = g.as_ref() {
return Some(format!("-QUIESCED migrating to {t}\r\n").into_bytes());
}
}
if IS_REPLICA.load(std::sync::atomic::Ordering::Relaxed) {
if READ_ONLY.load(std::sync::atomic::Ordering::Relaxed) {
return Some(b"-READONLY You can't write against a read only replica.\r\n".to_vec());
}
return None;
}
let min = MIN_REPLICAS.load(std::sync::atomic::Ordering::Relaxed);
if min > 0 && crate::ops::replication::healthy_replica_count() < min as usize {
return Some(b"-NOREPLICAS Not enough good replicas to write.\r\n".to_vec());
}
None
}
pub(crate) fn set_single_source(on: bool) {
SINGLE_SOURCE.store(on, std::sync::atomic::Ordering::Relaxed);
}
fn single_source() -> bool {
SINGLE_SOURCE.load(std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn current_upstream() -> Option<(IpAddr, u16)> {
*REPLICA_UPSTREAM
.lock()
.expect("REPLICA_UPSTREAM poisoned")
}
#[cfg(test)]
pub(crate) static TEST_STATE_GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_empty() {
let _g = TEST_STATE_GUARD.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
stop_runners();
install_senders(Vec::new());
assert!(senders_clone().is_empty());
assert!(current_upstream().is_none());
}
#[test]
fn applied_offset_sum_is_per_runner_sum_not_max() {
let _g = TEST_STATE_GUARD.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
stop_runners();
assert_eq!(applied_offset_sum(), 0, "no runners → 0");
*APPLIED_RUNNER_OFFSETS.lock().unwrap() = vec![0; 3];
record_ping(0, 1, 100, 40);
record_applied(1, 25);
record_applied(2, 35);
assert_eq!(applied_offset_sum(), 100, "sum across runners, not max");
record_applied(1, 5);
assert_eq!(applied_offset_sum(), 80);
record_applied(9, 1_000);
assert_eq!(applied_offset_sum(), 80);
stop_runners();
assert_eq!(applied_offset_sum(), 0);
}
#[test]
fn start_runners_without_senders_errors() {
let _g = TEST_STATE_GUARD.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
install_senders(Vec::new());
let result = start_runners((IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), 6400));
assert!(result.is_err());
}
}