use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use tokio::process::Child;
use tokio::sync::{RwLock, watch};
use super::{RecycleHooks, RecycleTrigger, TrackedClient};
use crate::embedder_client::EmbedderClient;
use crate::embedder_client::supervisor::SupervisorConfig;
const DRAIN_POLL: Duration = Duration::from_millis(20);
const DRAIN_MARGIN: Duration = Duration::from_secs(5);
pub(crate) struct RecycleCtx<'a> {
pub binary_path: &'a Path,
pub config: &'a SupervisorConfig,
pub hooks: &'a RecycleHooks,
pub child_slot: &'a tokio::sync::Mutex<Option<Child>>,
pub client_slot: &'a RwLock<Arc<dyn EmbedderClient>>,
pub child_pid_slot: &'a AtomicU32,
pub shutdown_rx: &'a watch::Receiver<bool>,
}
pub(crate) enum RecycleOutcome {
Swapped(Arc<TrackedClient>),
SpawnFailed(Arc<TrackedClient>),
OldExited(Arc<TrackedClient>),
}
pub(crate) async fn perform_recycle(
ctx: RecycleCtx<'_>,
trigger: RecycleTrigger,
old: Arc<TrackedClient>,
) -> RecycleOutcome {
let old_pid = ctx.child_pid_slot.load(Ordering::Acquire);
let mut child_guard = ctx.child_slot.lock().await;
let Some(old_child) = child_guard.as_mut() else {
return RecycleOutcome::OldExited(old);
};
let spawned = tokio::select! {
r = super::super::spawn_child(ctx.binary_path, ctx.config) => r,
_ = old_child.wait() => {
tracing::warn!(
trigger = trigger.as_str(),
old_pid,
"EmbedderSupervisor: sidecar exited while its recycle replacement was \
starting — handing it to the crash path"
);
return RecycleOutcome::OldExited(old);
}
};
let (new_child, new_client) = match spawned {
Ok(pair) => pair,
Err(e) => {
ctx.hooks
.stats
.spawn_failures
.fetch_add(1, Ordering::Relaxed);
tracing::warn!(
trigger = trigger.as_str(),
old_pid,
"EmbedderSupervisor: recycle replacement failed to start — keeping the \
current sidecar: {e:#}"
);
return RecycleOutcome::SpawnFailed(old);
}
};
let new_pid = new_child.id().unwrap_or(0);
let tracked = TrackedClient::new(new_client);
*ctx.client_slot.write().await = Arc::clone(&tracked) as Arc<dyn EmbedderClient>;
old.retire(Arc::clone(&tracked) as Arc<dyn EmbedderClient>);
let old_child = child_guard.replace(new_child);
drop(child_guard);
ctx.child_pid_slot.store(new_pid, Ordering::Release);
tracing::info!(
trigger = trigger.as_str(),
old_pid,
new_pid,
"EmbedderSupervisor: recycling sidecar (old pid={old_pid}, new pid={new_pid}, \
trigger={})",
trigger.as_str()
);
ctx.hooks.stats.record(trigger, old_pid, new_pid);
let stall = Duration::from_secs(ctx.config.recycle.drain_secs)
.max(crate::embedder_client::stdio::embed_call_timeout() + DRAIN_MARGIN);
drain(&old, stall, ctx.shutdown_rx).await;
drop(old);
if let Some(mut child) = old_child {
if let Err(e) = child.start_kill() {
tracing::debug!("EmbedderSupervisor: recycled sidecar already exited: {e}");
}
let _ = child.wait().await;
}
RecycleOutcome::Swapped(tracked)
}
async fn drain(old: &TrackedClient, stall: Duration, shutdown_rx: &watch::Receiver<bool>) {
let mut finished = old.finished.load(Ordering::Acquire);
let mut progress_at = tokio::time::Instant::now();
loop {
if old.in_flight.load(Ordering::SeqCst) == 0 || *shutdown_rx.borrow() {
return;
}
let now = tokio::time::Instant::now();
let done = old.finished.load(Ordering::Acquire);
if done != finished {
finished = done;
progress_at = now;
}
if now.duration_since(progress_at) >= stall {
tracing::warn!(
in_flight = old.in_flight.load(Ordering::SeqCst),
"EmbedderSupervisor: no call on the recycled sidecar finished in {stall:?} — \
stopping it"
);
return;
}
tokio::time::sleep(DRAIN_POLL).await;
}
}