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}