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
43 changes: 41 additions & 2 deletions container-runner/src/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! watchdogs unexpected child exits, `on_fetch`/`on_websocket` proxy tunneled
//! traffic to the child, and `on_sleep`/`on_destroy` stop it.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, LazyLock};

use anyhow::{Context, Result};
Expand All @@ -14,7 +15,7 @@
use crate::child::{ChildProcess, SpawnSpec, log_prefix};
use crate::input::ActorInput;
use crate::{
children, drain_grace, effective_stop_grace, exit_token, release_child_port,
children, drain_grace, effective_stop_grace, exit_token, idle_timeout, release_child_port,
request_exit, reserve_child_port, runner_config,
};

Expand All @@ -25,6 +26,9 @@

pub struct GameServer {
child: TokioMutex<Option<Arc<ChildProcess>>>,
/// Set on the first request, disarming the one-shot startup idle timer. Only
/// meaningful when [`idle_timeout`] is enabled.
got_request: AtomicBool,
}

impl GameServer {
Expand Down Expand Up @@ -77,6 +81,31 @@
}
self.stop_child(actor_id, reason).await;
}

/// Arm the one-shot startup idle timer when [`idle_timeout`] is set. After the
/// window, if no request has arrived, ask the actor to sleep (`stop_child` then
/// exits the container). Cancelled early if the actor starts shutting down.
fn arm_idle_timeout(self: &Arc<Self>, ctx: &Ctx<Self>, actor_id: String) {
let Some(timeout) = idle_timeout() else {
return;
};
let this = self.clone();
let ctx = ctx.clone();
tokio::spawn(async move {
let abort = ctx.abort_signal();
tokio::select! {
_ = tokio::time::sleep(timeout) => {}
_ = abort.cancelled() => return,
}
if this.got_request.load(Ordering::Relaxed) {
return;
}
tracing::info!(actor_id = %actor_id, ?timeout, "no request within idle timeout, sleeping");
if let Err(err) = ctx.sleep() {
tracing::debug!(error = ?err, actor_id = %actor_id, "idle sleep request failed");
}
});
}
}

#[async_trait]
Expand All @@ -99,6 +128,7 @@
async fn create(_ctx: &Ctx<Self>) -> Result<Self> {
Ok(Self {
child: TokioMutex::new(None),
got_request: AtomicBool::new(false),
})
}

Expand Down Expand Up @@ -206,6 +236,7 @@
// hook to remove the entry, so registering earlier would leak it.
register_ctx(&actor_id, &ctx).await;
*self.child.lock().await = Some(child);
self.arm_idle_timeout(&ctx, actor_id);
Ok(())
}

Expand Down Expand Up @@ -241,6 +272,7 @@
}

async fn on_fetch(self: Arc<Self>, ctx: Ctx<Self>, req: Request) -> Result<Response> {
self.got_request.store(true, Ordering::Relaxed);
let child_port = self
.child
.lock()
Expand All @@ -257,6 +289,7 @@
ws: WebSocket,
req: Request,
) -> Result<()> {
self.got_request.store(true, Ordering::Relaxed);
let child_port = self
.child
.lock()
Expand All @@ -274,8 +307,14 @@

/// Engine-initiated sleep. `no_sleep` blocks only idle sleep; the engine can
/// still sleep an actor (dashboard, crash policy, eviction), so we stop the child.
/// In idle-timeout mode the sleep is (treated as) an idle sleep, so it skips the
/// drain and stops promptly; otherwise it drains for in-flight work first.

Check warning on line 311 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
async fn on_sleep(self: Arc<Self>, ctx: Ctx<Self>) -> Result<()> {
self.drain_then_stop_child(ctx.actor_id(), "actor sleeping").await;
if idle_timeout().is_some() {
self.stop_child(ctx.actor_id(), "actor sleeping (idle)").await;
} else {
self.drain_then_stop_child(ctx.actor_id(), "actor sleeping").await;
}
Ok(())
}

Expand Down
16 changes: 16 additions & 0 deletions container-runner/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ pub fn drain_grace() -> Duration {
*DRAIN_GRACE
}

/// One-shot startup idle timeout. If the actor receives no request within this
/// window of starting, it sleeps (and the container exits). `None` when
/// RIVET_IDLE_TIMEOUT_SECS is unset or 0 (disabled); the first request disarms it.
static IDLE_TIMEOUT: LazyLock<Option<Duration>> = LazyLock::new(|| {
let secs = std::env::var("RIVET_IDLE_TIMEOUT_SECS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.unwrap_or(0);
(secs > 0).then(|| Duration::from_secs(secs))
});

/// The one-shot startup idle-sleep window, or `None` when disabled. See [`IDLE_TIMEOUT`].
pub fn idle_timeout() -> Option<Duration> {
*IDLE_TIMEOUT
}

/// Token that fires on a platform shutdown signal. Cuts a drain wait short so the
/// platform's SIGTERM→SIGKILL budget is honored.
pub fn exit_token() -> &'static CancellationToken {
Expand Down
Loading