use std::net::{Shutdown, TcpStream};
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::Duration;
use kevy_replicate::replica::{ReplicaClient, ReplicaEvent};
use kevy_rt::{ReplicaApply, ReplicaInboxSender};
const RECONNECT_BACKOFF: Duration = Duration::from_millis(250);
pub(crate) struct ReplicaRunner {
handle: Option<JoinHandle<()>>,
stop: Arc<AtomicBool>,
socket: Arc<Mutex<Option<TcpStream>>>,
}
impl ReplicaRunner {
pub(crate) fn spawn(
upstream_addr: (std::net::IpAddr, u16),
replica_id: String,
sender: ReplicaInboxSender,
) -> Self {
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let socket: Arc<Mutex<Option<TcpStream>>> = Arc::new(Mutex::new(None));
let socket_thread = socket.clone();
let handle = std::thread::Builder::new()
.name(format!("kevy-replica-{replica_id}"))
.spawn(move || {
run_loop(upstream_addr, replica_id, sender, stop_thread, socket_thread);
})
.expect("spawn replica runner thread");
Self {
handle: Some(handle),
stop,
socket,
}
}
#[allow(dead_code)] pub(crate) fn shutdown(mut self) {
self.signal_stop();
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
fn signal_stop(&self) {
self.stop.store(true, Ordering::Relaxed);
if let Ok(guard) = self.socket.lock()
&& let Some(s) = guard.as_ref()
{
let _ = s.shutdown(Shutdown::Both);
}
}
}
impl Drop for ReplicaRunner {
fn drop(&mut self) {
self.signal_stop();
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
fn run_loop(
upstream_addr: (std::net::IpAddr, u16),
replica_id: String,
sender: ReplicaInboxSender,
stop: Arc<AtomicBool>,
socket_slot: Arc<Mutex<Option<TcpStream>>>,
) {
let mut from_offset: u64 = 0;
while !stop.load(Ordering::Relaxed) {
match ReplicaClient::connect(upstream_addr, &replica_id, from_offset) {
Ok(mut client) => {
if let Ok(handle) = client.socket_handle()
&& let Ok(mut guard) = socket_slot.lock()
{
*guard = Some(handle);
}
from_offset = drain_client(&mut client, &sender, &stop);
if let Ok(mut guard) = socket_slot.lock() {
*guard = None;
}
}
Err(e) => {
eprintln!(
"kevy: replica runner '{replica_id}' connect to \
{upstream_addr:?} failed: {e}; retrying in \
{RECONNECT_BACKOFF:?}"
);
}
}
if !stop.load(Ordering::Relaxed) {
std::thread::sleep(RECONNECT_BACKOFF);
}
}
}
fn drain_client(
client: &mut ReplicaClient,
sender: &ReplicaInboxSender,
stop: &Arc<AtomicBool>,
) -> u64 {
let mut from_offset = client.expected_offset();
while !stop.load(Ordering::Relaxed) {
match client.next_event() {
Some(Ok(event)) => {
let apply = event_to_apply(event, &mut from_offset);
if sender.send(apply).is_err() {
return from_offset;
}
}
Some(Err(e)) => {
eprintln!("kevy: replica runner upstream error: {e}");
return from_offset;
}
None => return from_offset, }
}
from_offset
}
fn event_to_apply(event: ReplicaEvent, from_offset: &mut u64) -> ReplicaApply {
match event {
ReplicaEvent::SnapshotBegin => ReplicaApply::SnapshotBegin,
ReplicaEvent::SnapshotChunk(bytes) => ReplicaApply::SnapshotChunk(bytes),
ReplicaEvent::SnapshotEnd { ack_offset } => {
*from_offset = ack_offset;
ReplicaApply::SnapshotEnd { ack_offset }
}
ReplicaEvent::Frame(frame) => {
*from_offset = frame.offset.saturating_add(1);
ReplicaApply::Frame {
offset: frame.offset,
argv: frame.argv,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn event_to_apply_snapshot_begin_passthrough() {
let mut off = 7;
let out = event_to_apply(ReplicaEvent::SnapshotBegin, &mut off);
assert!(matches!(out, ReplicaApply::SnapshotBegin));
assert_eq!(off, 7, "SnapshotBegin must not touch the offset");
}
#[test]
fn event_to_apply_snapshot_end_advances_offset() {
let mut off = 0;
let out = event_to_apply(ReplicaEvent::SnapshotEnd { ack_offset: 42 }, &mut off);
match out {
ReplicaApply::SnapshotEnd { ack_offset } => assert_eq!(ack_offset, 42),
other => panic!("unexpected: {other:?}"),
}
assert_eq!(off, 42, "SnapshotEnd must jump from_offset to ack_offset");
}
#[test]
fn event_to_apply_frame_advances_offset_by_one() {
let mut off = 3;
let frame = kevy_replicate::replica::DecodedFrame {
offset: 9,
argv: kevy_rt::Argv::default(),
};
let out = event_to_apply(ReplicaEvent::Frame(frame), &mut off);
assert!(matches!(out, ReplicaApply::Frame { offset: 9, .. }));
assert_eq!(off, 10, "Frame must advance to offset + 1");
}
}