Skip to content
Open
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
22 changes: 15 additions & 7 deletions container-runner/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
use rivetkit::{Actor, ActorKeySegment, Ctx, Request, Response, WebSocket, action};
use tokio::sync::Mutex as TokioMutex;

use crate::child::{ChildProcess, SpawnSpec, log_prefix};

Check warning on line 17 in container-runner/src/actor.rs

View workflow job for this annotation

GitHub Actions / Rustfmt

Diff in /home/runner/work/actors/actors/container-runner/src/actor.rs
use crate::input::ActorInput;
use crate::{
children, drain_grace, effective_stop_grace, exit_token, release_child_port,
reserve_child_port, runner_config,
request_exit, reserve_child_port, runner_config,
};

/// Live actor contexts on this instance, keyed by actor id. Lets the process
Expand Down Expand Up @@ -48,12 +48,20 @@
release_child_port(child.child_port).await;
}

// The instance stays alive and warm after its last actor stops, ready to
// host the next placement. It is reaped by the platform's own shutdown
// signal, not by self-exit. This keeps the serverless container long
// lived enough for the log agent to drain its stderr, which a fast
// self-exit could otherwise lose.
tracing::info!(actor_id = %actor_id, reason, "actor stopped, keeping instance warm");
// Exit the whole process once the last child on this instance stops. The
// runner is PID 1, so `request_exit` cancels `EXIT`, which wakes `main`
// to run the graceful envoy close and then return, stopping the container
// so the platform reaps it. Guarded on an empty registry so a multi-actor
// instance does not tear down siblings still hosting a child.
if children().is_empty() {
request_exit(actor_id, reason);
} else {
tracing::info!(
actor_id = %actor_id,
reason,
"actor stopped, other actors still running on this instance"
);
}
}

/// Engine pause path (sleep, lost, going-away). Give the child up to
Expand Down Expand Up @@ -288,7 +296,7 @@

/// Engine-initiated sleep. `no_sleep` suppresses idle sleep, but the
/// engine can still sleep an actor (dashboard, crash policy, eviction
/// ahead of instance retirement); leaving the child running would orphan

Check warning on line 299 in container-runner/src/actor.rs

View workflow job for this annotation

GitHub Actions / Rustfmt

Diff in /home/runner/work/actors/actors/container-runner/src/actor.rs
/// it on an instance the engine considers vacated.
async fn on_sleep(self: Arc<Self>, ctx: Ctx<Self>) -> Result<()> {
self.drain_then_stop_child(ctx.actor_id(), "actor sleeping").await;
Expand Down
21 changes: 9 additions & 12 deletions container-runner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ pub fn effective_stop_grace() -> Duration {
*SIGTERM_BUDGET
}

/// End the process. Only the platform shutdown signal drives this now: actors
/// stopping or failing to start no longer exit the instance, so it stays warm
/// and reusable and its logs have time to drain. The runner is PID 1 in the
/// image, so exiting stops the container and the platform reaps the instance.
/// End the process. Driven by a platform shutdown signal or by the last child on
/// this instance stopping (see `stop_child`). The runner is PID 1 in the image,
/// so cancelling `EXIT` wakes `main` to run the graceful envoy close and return,
/// which stops the container and lets the platform reap the instance.
pub fn request_exit(actor_id: &str, reason: &str) {
tracing::info!(actor_id = %actor_id, reason, "shutting down container");
EXIT.cancel();
Expand Down Expand Up @@ -394,11 +394,7 @@ async fn async_main() -> Result<()> {
));
tracing::info!(port, "container-runner serverless front door listening");

// Wait for an exit request, then tear down. Only the signal path is live
// today: nothing calls `request_exit` except `spawn_signal_handler`, which
// sets `SIGNAL_SHUTDOWN` before cancelling `EXIT`, so the `else` branch is
// currently unreachable and kept only as a fallback for a future
// actor-driven exit.
// Wait for an exit request, then tear down. Two shapes depending on why:
//
// Signal (platform is reclaiming the instance): kill our children AND
// notify the engine at the SAME time, each bounded by the full SIGTERM
Expand All @@ -408,9 +404,10 @@ async fn async_main() -> Result<()> {
// immediately. Bounding the drain means an unreachable engine cannot eat
// the budget the children need.
//
// Fallback actor-driven exit (unreachable today): no platform deadline.
// Children are already reaped by the hooks (the sweep is a no-op backstop),
// and the runtime drains unbounded so the /start SSE flushes cleanly.
// Actor-driven exit (the last child stopped, so `stop_child` cancelled
// `EXIT`): no platform deadline. The child is already reaped by the hook
// that triggered the exit (the sweep is a no-op backstop), and the runtime
// drains unbounded so the /start SSE flushes cleanly.
EXIT.cancelled().await;
if SIGNAL_SHUTDOWN.load(Ordering::Acquire) {
// A platform SIGTERM reclaims this instance. Report every actor as crashed
Expand Down
Loading