Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions container-runner/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,24 +158,29 @@ impl Actor for GameServer {

/// Watchdog: waits for the child to exit. Deliberate stops remove the
/// child from the global registry first, so winning the `remove` race
/// means the exit was unexpected and the actor must be torn down.
/// means the exit was unexpected and the actor must be torn down. A clean
/// exit (code 0) destroys the actor; any other exit returns an error so
/// the framework reports an errored stop and the engine records the crash.
async fn run(self: Arc<Self>, ctx: Ctx<Self>) -> Result<()> {
let Some(child) = self.child.lock().await.clone() else {
anyhow::bail!("run: child process was never spawned");
};

let status = child.wait_exit().await;
let exit = child.wait_exit().await;

let actor_id = ctx.actor_id().to_string();
if children().remove_async(&actor_id).await.is_some() {
release_child_port(child.child_port).await;
let prefix = log_prefix(&actor_id, child.key.as_deref());
if !exit.success {
println!(
"{prefix} runner: child exited unexpectedly ({exit}), reporting errored stop"
);
anyhow::bail!("child exited unexpectedly ({exit})");
}
println!(
"{prefix} runner: child exited unexpectedly ({status}), reporting actor stopped"
"{prefix} runner: child exited unexpectedly ({exit}), reporting actor stopped"
);
// TODO: `ctx.destroy()` reports a clean stop, while the old envoy
// path reported an errored stop that fed the engine's crash
// policy. The rivetkit wrapper has no errored-stop API yet.
if let Err(err) = ctx.destroy() {
// The actor may already be stopping if the engine beat us to it.
tracing::debug!(error = ?err, actor_id = %actor_id, "destroy after child exit failed");
Expand Down
55 changes: 40 additions & 15 deletions container-runner/src/child.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,31 @@ use tokio::process::Command;
use tokio::sync::watch;
use tokio::time::{Instant, sleep};

/// Terminal state of a child process.
#[derive(Clone)]
pub struct ChildExit {
/// True when the child exited on its own with code 0. Signal terminations,
/// nonzero exits, and wait errors are all failures.
pub success: bool,
/// Human-readable status, e.g. "exit status: 1" or "signal: 9 (SIGKILL)".
pub status: String,
}

impl std::fmt::Display for ChildExit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.status)
}
}

/// A spawned child process, its listening port, and the actor identity used to
/// prefix its logs.
pub struct ChildProcess {
pub actor_id: String,
pub key: Option<String>,
pub child_port: u16,
pid: u32,
/// `None` while running, `Some(status_string)` once the child has exited.
exited_rx: watch::Receiver<Option<String>>,
/// `None` while running, `Some(exit)` once the child has exited.
exited_rx: watch::Receiver<Option<ChildExit>>,
}

/// Everything needed to launch the child.
Expand Down Expand Up @@ -100,16 +116,22 @@ impl ChildProcess {
println!("{prefix} runner: spawned `{program}` (pid={pid}) on child port {child_port}");

// Reaper task owns the Child and reports its exit status.
let (exited_tx, exited_rx) = watch::channel::<Option<String>>(None);
let (exited_tx, exited_rx) = watch::channel::<Option<ChildExit>>(None);
{
let prefix = prefix.clone();
tokio::spawn(async move {
let status = match child.wait().await {
Ok(status) => status.to_string(),
Err(e) => format!("wait error: {e}"),
let exit = match child.wait().await {
Ok(status) => ChildExit {
success: status.success(),
status: status.to_string(),
},
Err(e) => ChildExit {
success: false,
status: format!("wait error: {e}"),
},
};
println!("{prefix} runner: child process exited (status: {status})");
let _ = exited_tx.send(Some(status));
println!("{prefix} runner: child process exited (status: {exit})");
let _ = exited_tx.send(Some(exit));
});
}

Expand Down Expand Up @@ -138,8 +160,8 @@ impl ChildProcess {
let addr = (Ipv4Addr::LOCALHOST, self.child_port);
let prefix = log_prefix(&self.actor_id, self.key.as_deref());
loop {
if let Some(status) = self.exited_rx.borrow().clone() {
anyhow::bail!("child exited before becoming ready (status: {status})");
if let Some(exit) = self.exited_rx.borrow().clone() {
anyhow::bail!("child exited before becoming ready (status: {exit})");
}
if TcpStream::connect(addr).await.is_ok() {
println!(
Expand All @@ -163,15 +185,18 @@ impl ChildProcess {
self.exited_rx.borrow().is_some()
}

/// Wait until the child exits (on its own or via `stop`), returning its status.
pub async fn wait_exit(&self) -> String {
/// Wait until the child exits (on its own or via `stop`), returning its exit state.
pub async fn wait_exit(&self) -> ChildExit {
let mut rx = self.exited_rx.clone();
loop {
if let Some(status) = rx.borrow().clone() {
return status;
if let Some(exit) = rx.borrow().clone() {
return exit;
}
if rx.changed().await.is_err() {
return "reaper task ended".to_string();
return ChildExit {
success: false,
status: "reaper task ended".to_string(),
};
}
}
}
Expand Down
12 changes: 0 additions & 12 deletions engine/sdks/rust/envoy-client/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,6 @@ impl EnvoyHandle {
);
}

pub fn destroy_actor(&self, actor_id: String, generation: Option<u32>) {
let _ = crate::envoy::send_to_envoy_tx(
&self.shared,
ToEnvoyMessage::ActorIntent {
actor_id,
generation,
intent: protocol::ActorIntent::ActorIntentStop,
error: None,
},
);
}

pub async fn get_actor(&self, actor_id: &str, generation: Option<u32>) -> Option<ActorInfo> {
let (tx, rx) = tokio::sync::oneshot::channel();
crate::envoy::send_to_envoy_tx(
Expand Down
44 changes: 27 additions & 17 deletions frontend/packages/shared-data/src/deploy.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 40 additions & 7 deletions frontend/src/app/data-providers/cloud-data-provider.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading