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, SnapshotGate};
use crate::state::ReplicaProgress;
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,
runner_slot: usize,
progress: Arc<ReplicaProgress>,
) -> Self {
Self::spawn_target(upstream_addr, replica_id, Target::PerShard(sender), runner_slot, progress)
}
pub(crate) fn spawn_routed(
upstream_addr: (std::net::IpAddr, u16),
replica_id: String,
senders: Vec<ReplicaInboxSender>,
runner_slot: usize,
progress: Arc<ReplicaProgress>,
) -> Self {
Self::spawn_target(upstream_addr, replica_id, Target::Routed(senders), runner_slot, progress)
}
fn spawn_target(
upstream_addr: (std::net::IpAddr, u16),
replica_id: String,
target: Target,
runner_slot: usize,
progress: Arc<ReplicaProgress>,
) -> 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, target, stop_thread, socket_thread, runner_slot, progress);
})
.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();
}
}
}
enum Target {
PerShard(ReplicaInboxSender),
Routed(Vec<ReplicaInboxSender>),
}
fn run_loop(
upstream_addr: (std::net::IpAddr, u16),
replica_id: String,
target: Target,
stop: Arc<AtomicBool>,
socket_slot: Arc<Mutex<Option<TcpStream>>>,
runner_slot: usize,
progress: Arc<ReplicaProgress>,
) {
let mut from_offset: u64 = 0;
let mut data_gen: u64 = 0;
while !stop.load(Ordering::Relaxed) {
match ReplicaClient::connect_at(
upstream_addr,
&replica_id,
data_gen,
from_offset,
Duration::from_secs(5),
) {
Ok(mut client) => {
from_offset = drain_session(
&mut client, &target, &stop, &socket_slot, runner_slot,
&progress, &mut data_gen,
);
}
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_session(
client: &mut ReplicaClient,
target: &Target,
stop: &Arc<AtomicBool>,
socket_slot: &Mutex<Option<TcpStream>>,
runner_slot: usize,
progress: &Arc<ReplicaProgress>,
data_gen: &mut u64,
) -> u64 {
set_socket_slot(socket_slot, client.socket_handle().ok());
crate::replica_trace::trace_session_start(runner_slot, client, *data_gen);
let from_offset = match target {
Target::PerShard(sender) => {
drain_client(client, sender, stop, runner_slot, progress, data_gen)
}
Target::Routed(senders) => crate::replica_runner_routed::drain_client_routed(
client, senders, stop, runner_slot, progress, data_gen,
),
};
set_socket_slot(socket_slot, None);
from_offset
}
fn set_socket_slot(slot: &Mutex<Option<TcpStream>>, value: Option<TcpStream>) {
if let Ok(mut guard) = slot.lock() {
*guard = value;
}
}
pub(crate) struct LoadingToken {
progress: Arc<ReplicaProgress>,
}
impl Drop for LoadingToken {
fn drop(&mut self) {
self.progress.end_loading();
}
}
pub(crate) struct LoadingGuard {
progress: Arc<ReplicaProgress>,
token: Option<Arc<LoadingToken>>,
}
impl LoadingGuard {
pub(crate) fn new(progress: Arc<ReplicaProgress>) -> Self {
Self { progress, token: None }
}
pub(crate) fn observe(&mut self, event: &ReplicaEvent) -> Option<SnapshotGate> {
match event {
ReplicaEvent::SnapshotBegin if self.token.is_none() => {
self.progress.begin_loading();
self.token = Some(Arc::new(LoadingToken {
progress: Arc::clone(&self.progress),
}));
None
}
ReplicaEvent::SnapshotEnd { .. } => {
self.token.take().map(|t| SnapshotGate::new(t))
}
_ => None,
}
}
}
fn drain_client(
client: &mut ReplicaClient,
sender: &ReplicaInboxSender,
stop: &Arc<AtomicBool>,
runner_slot: usize,
progress: &Arc<ReplicaProgress>,
data_gen: &mut u64,
) -> u64 {
let mut from_offset = client.expected_offset();
let ack_gen = client.primary_gen_at_handshake();
if from_offset == 0 {
*data_gen = ack_gen;
}
let mut last_ack = std::time::Instant::now();
let mut loading = LoadingGuard::new(Arc::clone(progress));
let mut traced_first_frame = false;
while !stop.load(Ordering::Relaxed) {
match client.next_event() {
Some(Ok(ReplicaEvent::Ping { generation, primary_offset })) => {
progress.record_ping(runner_slot, generation, primary_offset, from_offset);
let _ = client.send_ack(from_offset);
last_ack = std::time::Instant::now();
if !gen_still_matches(generation, ack_gen) {
return from_offset;
}
}
Some(Ok(event)) => {
if matches!(event, ReplicaEvent::SnapshotEnd { .. }) {
*data_gen = ack_gen;
}
crate::replica_trace::trace_session_event(
runner_slot, &event, &mut traced_first_frame,
);
if forward_event(event, &mut from_offset, &mut loading, sender).is_err() {
return from_offset;
}
maybe_ack(client, progress, runner_slot, from_offset, &mut last_ack);
}
Some(Err(e)) => {
eprintln!("kevy: replica runner upstream error: {e}");
return from_offset;
}
None => return from_offset, }
}
from_offset
}
fn forward_event(
event: ReplicaEvent,
from_offset: &mut u64,
loading: &mut LoadingGuard,
sender: &ReplicaInboxSender,
) -> Result<(), ()> {
let gate = loading.observe(&event);
let mut apply = event_to_apply(event, from_offset);
if let ReplicaApply::SnapshotEnd { gate: g, .. } = &mut apply {
*g = gate;
}
sender.send(apply).map_err(|_| ())
}
pub(crate) fn maybe_ack(
client: &mut ReplicaClient,
progress: &Arc<ReplicaProgress>,
runner_slot: usize,
from_offset: u64,
last_ack: &mut std::time::Instant,
) {
if last_ack.elapsed() >= std::time::Duration::from_millis(100) {
let _ = client.send_ack(from_offset);
progress.record_applied(runner_slot, from_offset);
*last_ack = std::time::Instant::now();
}
}
pub(crate) fn gen_still_matches(heartbeat_gen: u64, ack_gen: u64) -> bool {
if heartbeat_gen == 0 || heartbeat_gen == ack_gen {
return true;
}
eprintln!(
"kevy: replica runner: primary feed generation moved \
{ack_gen} -> {heartbeat_gen} mid-stream; re-handshaking"
);
false
}
fn event_to_apply(event: ReplicaEvent, from_offset: &mut u64) -> ReplicaApply {
match event {
ReplicaEvent::Ping { .. } => ReplicaApply::SnapshotBegin,
ReplicaEvent::SnapshotBegin => ReplicaApply::SnapshotBegin,
ReplicaEvent::SnapshotChunk(bytes) => ReplicaApply::SnapshotChunk(bytes),
ReplicaEvent::SnapshotEnd { ack_offset } => {
*from_offset = ack_offset;
ReplicaApply::SnapshotEnd { ack_offset, routed: false, gate: None }
}
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 loading_lowers_only_when_the_apply_gate_drops() {
let progress = Arc::new(ReplicaProgress::default());
let mut guard = LoadingGuard::new(Arc::clone(&progress));
assert!(guard.observe(&ReplicaEvent::SnapshotBegin).is_none());
assert!(progress.loading(), "SnapshotBegin raises the gate");
let gate = guard
.observe(&ReplicaEvent::SnapshotEnd { ack_offset: 9 })
.expect("SnapshotEnd must hand back the gate");
assert!(progress.loading(), "wire-read alone must not lower");
let second_shard = gate.clone(); drop(gate);
assert!(progress.loading(), "one shard's copy still alive");
drop(second_shard);
assert!(!progress.loading(), "last apply lowers the gate");
}
#[test]
fn early_exit_drop_lowers_loading() {
let progress = Arc::new(ReplicaProgress::default());
let mut guard = LoadingGuard::new(Arc::clone(&progress));
let _ = guard.observe(&ReplicaEvent::SnapshotBegin);
assert!(progress.loading());
drop(guard); assert!(!progress.loading(), "mid-ship exit never strands -LOADING");
}
#[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");
}
}