use std::net::{Shutdown, TcpStream};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;
use kevy_persist::Argv;
use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
use crate::store::{Shards, lock_write};
pub(crate) struct ReplicaRunner {
stop: Arc<AtomicBool>,
sock_clone: Arc<Mutex<Option<TcpStream>>>,
upstream: Arc<Mutex<String>>,
force_reconnect: Arc<AtomicBool>,
join: Mutex<Option<JoinHandle<()>>>,
#[allow(dead_code)]
pub(crate) applied_offset: Arc<AtomicU64>,
#[allow(dead_code)]
pub(crate) link_up: Arc<AtomicBool>,
}
impl ReplicaRunner {
pub(crate) fn spawn(
shards: Shards,
upstream: String,
replica_id: String,
backoff_min: Duration,
backoff_max: Duration,
) -> Self {
let stop = Arc::new(AtomicBool::new(false));
let sock_clone = Arc::new(Mutex::new(None::<TcpStream>));
let upstream_slot = Arc::new(Mutex::new(upstream));
let force_reconnect = Arc::new(AtomicBool::new(false));
let applied_offset = Arc::new(AtomicU64::new(0));
let link_up = Arc::new(AtomicBool::new(false));
let stop_c = Arc::clone(&stop);
let sock_c = Arc::clone(&sock_clone);
let upstream_c = Arc::clone(&upstream_slot);
let force_c = Arc::clone(&force_reconnect);
let offset_c = Arc::clone(&applied_offset);
let link_c = Arc::clone(&link_up);
let join = thread::Builder::new()
.name("kevy-embedded-replica".into())
.spawn(move || {
run_loop(
shards,
upstream_c,
replica_id,
stop_c,
sock_c,
force_c,
offset_c,
link_c,
backoff_min,
backoff_max,
);
})
.expect("kevy-embedded: failed to spawn replica runner thread");
Self {
stop,
sock_clone,
upstream: upstream_slot,
force_reconnect,
join: Mutex::new(Some(join)),
applied_offset,
link_up,
}
}
pub(crate) fn set_upstream(&self, new_upstream: String) {
*self
.upstream
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = new_upstream;
self.applied_offset.store(0, Ordering::Relaxed);
self.force_reconnect.store(true, Ordering::Relaxed);
if let Some(s) = self
.sock_clone
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
{
let _ = s.shutdown(Shutdown::Both);
}
}
pub(crate) fn shutdown(&self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(s) = self
.sock_clone
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
{
let _ = s.shutdown(Shutdown::Both);
}
if let Some(j) = self
.join
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
let _ = j.join();
}
}
}
#[allow(clippy::too_many_arguments)]
fn run_loop(
shards: Shards,
upstream: Arc<Mutex<String>>,
replica_id: String,
stop: Arc<AtomicBool>,
sock_clone: Arc<Mutex<Option<TcpStream>>>,
force_reconnect: Arc<AtomicBool>,
applied_offset: Arc<AtomicU64>,
link_up: Arc<AtomicBool>,
backoff_min: Duration,
backoff_max: Duration,
) {
let mut backoff = backoff_min;
while !stop.load(Ordering::Relaxed) {
force_reconnect.store(false, Ordering::Relaxed);
let from_offset = applied_offset.load(Ordering::Relaxed);
let target = upstream
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
match ReplicaClient::connect(&target, &replica_id, from_offset) {
Ok(mut client) => {
if let Ok(s) = client.socket_handle() {
*sock_clone
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(s);
}
link_up.store(true, Ordering::Relaxed);
backoff = backoff_min;
drain_session(&shards, &mut client, &stop, &applied_offset);
link_up.store(false, Ordering::Relaxed);
*sock_clone
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
}
Err(_) => {
sleep_interruptible(&stop, backoff, backoff_min);
backoff = (backoff * 2).min(backoff_max);
}
}
}
}
fn drain_session(
shards: &Shards,
client: &mut ReplicaClient,
stop: &Arc<AtomicBool>,
applied_offset: &Arc<AtomicU64>,
) {
let mut snap: Option<Vec<u8>> = None;
while !stop.load(Ordering::Relaxed) {
match client.next_event() {
Some(Ok(ReplicaEvent::Frame(frame))) => {
if snap.is_some() {
break;
}
apply_frame(shards, &frame.argv);
applied_offset.store(client.expected_offset(), Ordering::Relaxed);
}
Some(Ok(ReplicaEvent::SnapshotBegin)) => {
for shard in shards.iter() {
let mut g = lock_write(shard);
g.store.flushall();
}
snap = Some(Vec::new());
}
Some(Ok(ReplicaEvent::SnapshotChunk(bytes))) => {
if let Some(buf) = snap.as_mut() {
buf.extend_from_slice(&bytes);
}
}
Some(Ok(ReplicaEvent::SnapshotEnd { ack_offset })) => {
if let Some(buf) = snap.take() {
if !load_snapshot_into_shard0(shards, &buf) {
break;
}
applied_offset.store(ack_offset, Ordering::Relaxed);
}
}
Some(Err(_)) | None => break,
}
}
}
fn apply_frame(shards: &Shards, argv: &Argv) {
let n = shards.len();
let idx = route_shard(argv, n);
let shard = &shards[idx];
let mut g = lock_write(shard);
crate::replay::apply(&mut g.store, argv);
}
fn load_snapshot_into_shard0(shards: &Shards, payload: &[u8]) -> bool {
let shard = &shards[0];
let mut g = lock_write(shard);
let cursor = std::io::Cursor::new(payload);
kevy_persist::load_snapshot_from(&mut g.store, cursor).is_ok()
}
fn route_shard(argv: &Argv, n: usize) -> usize {
if n <= 1 {
return 0;
}
let Some(key) = argv.get(1) else {
return 0;
};
(kevy_hash::key_hash_slot(key) as usize) % n
}
fn sleep_interruptible(stop: &Arc<AtomicBool>, dur: Duration, slice: Duration) {
let mut remaining = dur;
while !stop.load(Ordering::Relaxed) && remaining > Duration::ZERO {
let chunk = remaining.min(slice);
thread::sleep(chunk);
remaining = remaining.saturating_sub(chunk);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn argv(parts: &[&[u8]]) -> Argv {
let mut a = Argv::default();
for p in parts {
a.push(p);
}
a
}
#[test]
fn route_keyless_goes_to_shard_zero() {
let a = argv(&[b"FLUSHALL"]);
assert_eq!(route_shard(&a, 4), 0);
}
#[test]
fn route_single_shard_always_zero() {
let a = argv(&[b"SET", b"any-key", b"v"]);
assert_eq!(route_shard(&a, 1), 0);
}
#[test]
fn route_keyed_is_deterministic_by_key_hash() {
let a = argv(&[b"SET", b"k1", b"v"]);
let b = argv(&[b"DEL", b"k1"]);
assert_eq!(route_shard(&a, 8), route_shard(&b, 8));
}
}