Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge remote-tracking branch 'origin/main' into workspace-chat | 1 | /** |
| 2 | * How a sandbox's Durable Object handles the end of its container without | |
| 3 | * failing the invocation it happens in. The containers library calls | |
| 4 | * `onStop` from the object's alarm: a stop hook that throws fails that | |
| 5 | * alarm, which Cloudflare retries and counts as an error each time, and | |
| 6 | * which runs the whole hook again. So the hook's calls to other services | |
| 7 | * go through `tellStopped`, which tries twice and logs at error level, and | |
| 8 | * never throws. Pure, so it is tested on its own. | |
| 9 | */ | |
| 10 | ||
| 11 | /** How long `tellStopped` waits before its second try. */ | |
| 12 | export const RETRY_AFTER_MS = 500; | |
| 13 | ||
| 14 | /** | |
| 15 | * Tells another service that a sandbox stopped: `step` runs, and once more | |
| 16 | * if it throws. Never throws: a step that fails twice is logged with | |
| 17 | * `log` as `sandbox stop not reported`, with what and why, so it shows in | |
| 18 | * the runner's logs at error level and the five-minute sweep picks the | |
| 19 | * work up instead. Returns whether it got through. | |
| 20 | */ | |
| 21 | export async function tellStopped( | |
| 22 | what: string, | |
| 23 | step: () => Promise<unknown>, | |
| 24 | options: { log?: (...data: unknown[]) => void; attempts?: number; waitMs?: number } = {}, | |
| 25 | ): Promise<boolean> { | |
| 26 | const log = options.log ?? console.error; | |
| 27 | const attempts = Math.max(1, options.attempts ?? 2); | |
| 28 | let last: unknown = null; | |
| 29 | for (let attempt = 1; attempt <= attempts; attempt++) { | |
| 30 | try { | |
| 31 | await step(); | |
| 32 | return true; | |
| 33 | } catch (error) { | |
| 34 | last = error; | |
| 35 | if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, options.waitMs ?? RETRY_AFTER_MS)); | |
| 36 | } | |
| 37 | } | |
| 38 | log("sandbox stop not reported", what, describeError(last)); | |
| 39 | return false; | |
| 40 | } | |
| 41 | ||
| 42 | /** | |
| 43 | * A service binding's answer as a step's outcome: a 5xx (or 429) throws, so | |
| 44 | * `tellStopped` tries again and logs it. A refusal is not a failure: the | |
| 45 | * work already reported its end, and services say so with `ok: false` or a | |
| 46 | * 4xx (the deployments service answers 404 for a build that has finished). | |
| 47 | */ | |
| 48 | export async function answered(what: string, response: Response): Promise<void> { | |
| 49 | if (response.status < 500 && response.status !== 429) return; | |
| 50 | const body = await response.text().catch(() => ""); | |
| 51 | throw new Error(`${what} answered ${response.status}${body ? `: ${body.slice(0, 200)}` : ""}`); | |
| 52 | } | |
| 53 | ||
| 54 | /** | |
| 55 | * Whether a sandbox whose `sleepAfter` has passed is still inside its run's | |
| 56 | * time cap, and so keeps running: the cap, not inactivity, ends a run (the | |
| 57 | * runner never fetches the container, so to the library every sandbox looks | |
| 58 | * idle). Without a cap or a start time, `sleepAfter` applies. | |
| 59 | */ | |
| 60 | export function withinTimeCap( | |
| 61 | started: number | null | undefined, | |
| 62 | capMinutes: number | null | undefined, | |
| 63 | now: number, | |
| 64 | graceSeconds: number, | |
| 65 | ): boolean { | |
| 66 | if (typeof started !== "number" || typeof capMinutes !== "number" || capMinutes <= 0) return false; | |
| 67 | return now < started + (capMinutes * 60 + graceSeconds) * 1000; | |
| 68 | } | |
| 69 | ||
| 70 | /** An error as one line, for a log: its message, or what was thrown. */ | |
| 71 | export function describeError(error: unknown): string { | |
| 72 | if (error instanceof Error) return error.message || error.name; | |
| 73 | if (error === undefined) return "no reason given"; | |
| 74 | if (typeof error === "string") return error; | |
| 75 | try { | |
| 76 | return JSON.stringify(error) ?? String(error); | |
| 77 | } catch { | |
| 78 | return String(error); | |
| 79 | } | |
| 80 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.