Skip to main content

arcbox_computer_runtime/sandbox/
timers.rs

1//! Lifecycle deadlines: the hard TTL cap and idle detection (CORE-21/60).
2//!
3//! Two independent knobs, never conflated:
4//!
5//! - **TTL** (`ttl_deadline`) caps total lifetime regardless of activity and
6//!   always destroys — pausing does not apply, and a paused computer still
7//!   expires. `SetLifecycle` replaces the deadline from *now*.
8//! - **Idle** (`idle_timeout_seconds` + `on_idle`) reacts to inactivity: the
9//!   window is armed on every `Ready` edge (boot ready, workload exit,
10//!   resume) and cancelled when a workload starts. Idle means "no running
11//!   workload" — file activity does NOT re-arm.
12//!
13//! Both live in the computer's actor: they are two `Sleep`s in its select
14//! loop, armed and cancelled by the same transitions that publish. What is
15//! left here is the verb that replaces them, because it also has a durable
16//! record to write — the timers are in memory, so the record is the only
17//! thing a restart can re-arm them from.
18//!
19//! The apparatus this module used to hold — epoch-stamped timer slots, a
20//! `Weak` back to the instance a detached expiry task was armed for, a
21//! monitor driving all of it off the event stream, and a lag resync — existed
22//! only to answer "is this detached task still talking about the current
23//! generation?". A per-computer actor *is* the generation.
24
25use super::*;
26
27impl ComputerManager {
28    /// Replace a computer's lifecycle deadlines (CORE-60).
29    ///
30    /// `ttl_seconds` re-arms the hard cap from *now* (0 removes it);
31    /// `idle_timeout_seconds` replaces the idle window, re-arming any live
32    /// timer; `on_idle` replaces the policy. `None` fields are unchanged.
33    /// Allowed in any non-terminal state — a paused computer keeps honoring
34    /// its (re-armed) TTL, and new idle knobs apply on the next `Ready`.
35    pub async fn set_computer_lifecycle(
36        &self,
37        id: &ComputerId,
38        update: LifecycleUpdate,
39    ) -> Result<()> {
40        self.await_reconcile().await?;
41        let computer = self.computer(id)?;
42        // The patch goes to the actor, which resolves it against what the
43        // computer currently has. Resolving it here would let two concurrent
44        // partial updates each read the same policy and send a whole one, so
45        // the second would undo the first's field — which `None` means
46        // unchanged promises it does not.
47        computer
48            .mailbox
49            .ask(id, |reply| Command::SetLifecycle { update, reply })
50            .await?;
51        let deadlines = computer.snapshot.borrow().deadlines;
52        info!(
53            sandbox_id = %id,
54            ttl_deadline = ?deadlines.ttl,
55            idle_timeout_seconds = deadlines.idle_timeout_seconds,
56            on_idle = ?deadlines.on_idle,
57            "sandbox lifecycle updated"
58        );
59        Ok(())
60    }
61}