Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 23 additions & 6 deletions cell/src/computer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,27 @@ const SIGSTOP: f64 = 19.0;
/// A marker's exec answers within this, or it failed: a sleep goes on
/// without its hold, a start fails.
const MARK_EXEC_MS: i64 = 30_000;
/// A Durable Object's alarm runs at most this long (Cloudflare's limits:
/// "Alarm handler invocations have a maximum wall time of 15 minutes"),
/// and a save's steps run inside the alarm whose events asked for them
/// (`drive_events`): an idle sleep's hold, save and stop, an awake save's
/// hold, save and unhold, all in one.
const ALARM_WALL_MS: i64 = 15 * 60_000;
/// What an alarm's other events take beside those steps (its adopt, a
/// meter's flush, its owner told), each a call of about a second.
const ALARM_OTHER_MS: i64 = 60_000;
/// A save's backups, its records one after the other, answer within this
/// together, or the save fails (entry.mjs's `Overdue`), as one that says so
/// does: spike S3 moved 52 MB in 3.3 s, so a few GiB fit, and the rest of
/// the save still reports before the lifecycle's `SAVE_DEADLINE_MS`. The
/// Sandbox SDK "never times out anything" (r4, r6: a sick container's
/// backup never returned, so its sleep never ended).
const BACKUPS_MS: i64 = 8 * 60_000;
/// does: the Sandbox SDK "never times out anything" (r4, r6: a sick
/// container's backup never returned, so its sleep never ended). It is
/// what an alarm leaves after a sleep's other steps, the most any save can
/// have. A 20 GB disk holds about 17 GB of a person's (a fresh computer
/// uses 2.4 GB), 10.8 GB saved at the 1.6 to 1 the largest save showed;
/// 577 saves over 1 GB on production moved 9 to 156 MB a second
/// (2026-10-27 to 11-03), so eight minutes would cut 18% of such a save's
/// tries and this cuts 6%, each tried again a minute later. A save bigger
/// or slower than an alarm allows needs saves that are not whole copies.
const BACKUPS_MS: i64 = ALARM_WALL_MS - ALARM_OTHER_MS - HOLD_STEPS_MS - MARK_EXEC_MS - FORGET_PASS_MS - STOP_STEPS_MS;
/// A start's restores, its save's records together, answer within this, or
/// the start failed (tried again, on the same save): what the start's own
/// deadline leaves after the waits around them (the container taking an
Expand Down Expand Up @@ -231,7 +245,10 @@ const _: () = assert!(SAVE_STEPS_MS < SAVE_DEADLINE_MS, "a save reports before i
const _: () = assert!(STOP_STEPS_MS < SLEEP_DEADLINE_MS, "a stop reports before its deadline");
const _: () = assert!(EXEC_READY_MS + RESTORE_MS + CHECK_EXEC_MS + MARK_EXEC_MS <= START_DEADLINE_MS && ALIVE_MS < START_DEADLINE_MS, "a start's waits fit its deadline");
const _: () = assert!(RESTORE_MS >= 10 * 60_000, "a restore of a full disk's save (5.9 GB took longer than 165 s) is given ten minutes at least");
const _: () = assert!(SLEEP_STEPS_MS <= 11 * 60_000, "docs/api.md: a sleep answers within 11 minutes");
// an awake save's unhold (`MARK_EXEC_MS`) is within a stop's bound
const _: () = assert!(SLEEP_STEPS_MS + ALARM_OTHER_MS <= ALARM_WALL_MS, "a sleep's steps, and an alarm's other events, fit the one alarm they run in");
const _: () = assert!(BACKUPS_MS >= 11 * 60_000, "a full disk's save (10.8 GB at 16.1 MB/s) is given eleven minutes at least");
const _: () = assert!(SLEEP_STEPS_MS <= 14 * 60_000, "docs/api.md: a sleep answers within 14 minutes");
/// The largest request body the egress signs and hands on (it is hashed
/// for NIP-98 first, so it is read whole).
const EGRESS_BODY_MAX_BYTES: usize = 32 * 1024 * 1024;
Expand Down
15 changes: 8 additions & 7 deletions crates/core/src/computer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,12 @@ pub const HOLD_WAIT_MS: i64 = 20_000;
/// A hold (the touch, the wait, the answer) reports within this, or it is
/// asked again.
pub const HOLD_DEADLINE_MS: i64 = 90_000;
/// A save reports within this, or it failed, as one that says so did
/// (spike S3: 52 MB in 3.3 s, so a few GiB fit). The cell's own bounds on
/// a save's steps end it sooner (cell/src/computer.rs, `SAVE_STEPS_MS`):
/// this catches a report its isolate lost.
pub const SAVE_DEADLINE_MS: i64 = 10 * 60_000;
/// A save reports within this, or it failed, as one that says so did. The
/// cell's own bounds on a save's steps end it sooner (cell/src/computer.rs,
/// `SAVE_STEPS_MS`: 12 minutes 10 seconds, its backups what a Durable
/// Object's alarm leaves them, so a full disk's save fits): this catches a
/// report its isolate lost.
pub const SAVE_DEADLINE_MS: i64 = 13 * 60_000;
/// Saves of `/data` kept: a wake restores the newest that restores, and
/// falls back to the one before it (docs/computers.md, "Saves and what a
/// wake restores"). Beside these, the save its running life came up from
Expand Down Expand Up @@ -3354,7 +3355,7 @@ mod tests {
keep(&s, &mut metered);
assert_eq!(s.actions, vec![Action::Unhold { generation: g }], "failed: kept, held no more, and nothing metered since its try began (its alarm metered to it)");
assert_eq!(l.phase, Phase::Awake { generation: g, since_ms: first }, "the same container, awake");
assert!(matches!(&s.note, SaveNote::Says(why) if why.contains("could not save") && why.contains(&format!("save {hung} did not report within 10 minutes"))), "{:?}", s.note);
assert!(matches!(&s.note, SaveNote::Says(why) if why.contains("could not save") && why.contains(&format!("save {hung} did not report within {} minutes", SAVE_DEADLINE_MS / 60_000))), "{:?}", s.note);
let (tried, bound) = (T + IDLE_MS, first + rules.unsaved_max_ms);
assert_eq!(l.unmetered, Some((tried, bound)), "free from its try's start, the hung save's minutes included, to the bound after its failure");
let u = l.unsaved(&rules).expect("a failure is counted");
Expand Down Expand Up @@ -3459,7 +3460,7 @@ mod tests {
};
let slept = T + 60_000;

// a hung first save: free from its try's start, ten minutes before
// a hung first save: free from its try's start, its deadline before
// it failed; the bound and its stop time from the failure
let (mut l, _, _) = sleeping(slept);
let failed = slept + 500 + SAVE_DEADLINE_MS;
Expand Down
2 changes: 1 addition & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1888,7 +1888,7 @@ docs/computers.md's; these routes are its owner's. A view is
| `GET /api/computers` | a person | → `{computers: [ComputerView], support}`, `support` `FRAGMENT_SUPPORT_URL` or `null` |
| `GET /api/computers/{id}` | its owner (anyone else 404) | → `ComputerView`. Their pages are told when it changes (`watch`'s `changed`) |
| `POST /api/computers/{id}/wake` | its owner | → the view once awake; 503 `wont_wake` when it would not start |
| `POST /api/computers/{id}/sleep` | its owner | → the view once asleep, or awake with its `why` when its save failed; within the sum of its steps' bounds (`SLEEP_STEPS_MS`), at most 11 minutes |
| `POST /api/computers/{id}/sleep` | its owner | → the view once asleep, or awake with its `why` when its save failed; within the sum of its steps' bounds (`SLEEP_STEPS_MS`), at most 14 minutes |
| `POST /api/computers/{id}/restart` | its owner | `{generation?}` → the view once started again, fresh from the image and its newest good save. `generation` names the start the owner saw: one already restarted is not restarted again. At zero credit 402, a guest 403 |
| `POST /api/computers/{id}/notices/seen` | its owner | `{life}` → the view, that `went_back` notice told no more; a `life` past the newest, or 0, 400 |
| `POST /api/computers/{id}/notices/later` | its owner | `{version}` → the view, that update put off; not its next start's, or not 12 hex, 400 |
Expand Down
11 changes: 9 additions & 2 deletions docs/computers.md
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,15 @@ from a snapshot.
Every step the DO takes in a container ends within a bound, and one past
it fails (the Sandbox SDK times out nothing): the consts beside
`SLEEP_STEPS_MS` in cell/src/computer.rs, each within the lifecycle's
deadline for its phase, checked as the cell compiles. So an owner's sleep
answers within 11 minutes.
deadline for its phase, checked as the cell compiles. A save's steps run
in the Durable Object alarm whose events asked for them, which Cloudflare
ends at 15 minutes: a sleep's hold, save and stop fit one alarm, and its
backups get what the rest leaves (`BACKUPS_MS`, 11 minutes 10 seconds).
That is the most any save can have, so it is fixed, not one that follows
a save's progress. Saves over 1 GB moved 9 to 156 MB/s on production
(2026-10-27 to 11-03), so a full disk's save (about 10.8 GB) fits all but
the slowest 6% of tries, each tried again. So an owner's sleep answers
within 14 minutes.

### The fragment API

Expand Down
Loading