@coreplane/switchboard 1.234.0 → 1.236.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/Dockerfile +11 -0
- package/dist/assets/config/config.example.yaml +9 -4
- package/dist/assets/deploy/cloudflare-memory/worker.ts +12 -0
- package/dist/assets/deploy/cloudflare-resident/Dockerfile +18 -0
- package/dist/assets/deploy/cloudflare-resident/gc.ts +6 -9
- package/dist/assets/deploy/cloudflare-resident/worker.ts +225 -119
- package/dist/assets/deploy/cloudflare-sandbox/Dockerfile +11 -0
- package/dist/assets/deploy/cloudflare-sandbox/worker.ts +143 -13
- package/dist/assets/package-lock.json +212 -3
- package/dist/assets/package.json +4 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/coordinator/contract.ts +24 -4
- package/dist/assets/src/core/coordinator/driver.ts +31 -3
- package/dist/assets/src/core/runEvents.ts +43 -1
- package/dist/assets/src/core/runFriction.ts +1 -0
- package/dist/assets/src/core/ship/contract.ts +3 -1
- package/dist/assets/src/core/ship/coordinator.ts +213 -29
- package/dist/assets/src/core/trace/types.ts +5 -0
- package/dist/assets/src/execution/residentCleanliness.ts +24 -0
- package/dist/assets/src/execution/residentDisk.ts +21 -10
- package/dist/assets/src/execution/residentDiskBudget.ts +7 -6
- package/dist/assets/src/execution/sandboxErrors.ts +77 -0
- package/dist/assets/src/execution/sandboxIdle.ts +220 -0
- package/dist/assets/src/execution/sandboxStart.ts +90 -0
- package/dist/assets/web/dist/.vite/manifest.json +437 -422
- package/dist/assets/web/dist/assets/{AppShell-lYVcj-k7.js → AppShell-DZdz_sl7.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-D-v8am88.js → CostsPage-BfJsol9y.js} +1 -1
- package/dist/assets/web/dist/assets/{DeliveryPage-CvlWP7Eq.js → DeliveryPage-CvInfu7y.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-BzWS4Ca1.js → NotFoundPage-d37EP8hS.js} +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-DQSD0SZD.js +1 -0
- package/dist/assets/web/dist/assets/ResidentsIndexPage-CZUGLen6.js +1 -0
- package/dist/assets/web/dist/assets/RunFoldRow-Bngn4GjF.js +9 -0
- package/dist/assets/web/dist/assets/RunRoutePage-a-tMrirB.js +6 -0
- package/dist/assets/web/dist/assets/RunsIndexPage-s0VKmRda.js +1 -0
- package/dist/assets/web/dist/assets/{RunsTabs-DEICQ4FY.js → RunsTabs-CyCdOhJ2.js} +1 -1
- package/dist/assets/web/dist/assets/ScheduledPage-ByCmyrDs.js +1 -0
- package/dist/assets/web/dist/assets/SettingsPage-DhKEokGK.js +1 -0
- package/dist/assets/web/dist/assets/{StatusDot-Ct6AtHhS.js → StatusDot-D-BPSPpw.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-qol5Boof.js → Tooltip-DPDx5tXT.js} +1 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-0eiekTHQ.js +1 -0
- package/dist/assets/web/dist/assets/{angular-html-Cw130Zyi.js → angular-html-Ni1Xil2G.js} +1 -1
- package/dist/assets/web/dist/assets/{angular-ts-DY1m4C2s.js → angular-ts-oCgo6p05.js} +1 -1
- package/dist/assets/web/dist/assets/{apl-W6Ty7v05.js → apl-CiWYl0gh.js} +1 -1
- package/dist/assets/web/dist/assets/{astro-DlLVHwVk.js → astro-B-VeKZ_J.js} +1 -1
- package/dist/assets/web/dist/assets/{blade-Bco3tDh4.js → blade-BIWEISKd.js} +1 -1
- package/dist/assets/web/dist/assets/{c-1LaFY_cj.js → c-DHxA5Byp.js} +1 -1
- package/dist/assets/web/dist/assets/{chapel-B-OvrOL5.js → chapel-D4qGLw-Q.js} +1 -1
- package/dist/assets/web/dist/assets/{cobol-Ch3EPlHu.js → cobol-BznX5Q-u.js} +1 -1
- package/dist/assets/web/dist/assets/{coffee-hZuITnto.js → coffee-CVr8Idlj.js} +1 -1
- package/dist/assets/web/dist/assets/{cpp-B8iRVo8m.js → cpp-wrk6NfW4.js} +1 -1
- package/dist/assets/web/dist/assets/{crystal-DKveI7lr.js → crystal-CGYWMeH1.js} +1 -1
- package/dist/assets/web/dist/assets/{css-_2_CD-rr.js → css-BgGytMsy.js} +1 -1
- package/dist/assets/web/dist/assets/{dist-DwtJj-ou.js → dist-DSkVB16z.js} +2 -2
- package/dist/assets/web/dist/assets/durationTone-DYogEPEk.js +1 -0
- package/dist/assets/web/dist/assets/{edge-BenMDPct.js → edge-CdR6AU9v.js} +1 -1
- package/dist/assets/web/dist/assets/{elixir-CBbdEqG6.js → elixir-CQRb3PZx.js} +1 -1
- package/dist/assets/web/dist/assets/{elm-DMub_2lY.js → elm-CpuA6W8B.js} +1 -1
- package/dist/assets/web/dist/assets/{erb-CI9He_Up.js → erb-CvlrblqH.js} +1 -1
- package/dist/assets/web/dist/assets/{favicon-BQsePYv5.js → favicon-CSt-qDvc.js} +1 -1
- package/dist/assets/web/dist/assets/{git-rebase-BBfhnyF8.js → git-rebase-COnovCVc.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-js-CR9Fxrau.js → glimmer-js-C-DS4g_J.js} +1 -1
- package/dist/assets/web/dist/assets/{glimmer-ts-CwFwX_XS.js → glimmer-ts-DTkorWRI.js} +1 -1
- package/dist/assets/web/dist/assets/{glsl-Daiodp1r.js → glsl-CH51-JW7.js} +1 -1
- package/dist/assets/web/dist/assets/{graphql-DZvrsEkB.js → graphql-BO-XymNv.js} +1 -1
- package/dist/assets/web/dist/assets/{hack-BaXOzdg7.js → hack-C-N0oicf.js} +1 -1
- package/dist/assets/web/dist/assets/{haml-CMskq_VA.js → haml-BfmdtWoP.js} +1 -1
- package/dist/assets/web/dist/assets/{handlebars-Dhw_f7jD.js → handlebars-BqhO_te-.js} +1 -1
- package/dist/assets/web/dist/assets/{html-C9k99z0z.js → html-DUEZlTKV.js} +1 -1
- package/dist/assets/web/dist/assets/{html-derivative-VckPItXN.js → html-derivative-DOUJTS8A.js} +1 -1
- package/dist/assets/web/dist/assets/{http-DXU_3h9v.js → http-j7UH0Cqv.js} +1 -1
- package/dist/assets/web/dist/assets/{hurl-KU24JNdp.js → hurl-D6ZaBLJk.js} +1 -1
- package/dist/assets/web/dist/assets/indexRow-BjXBzI5Z.js +1 -0
- package/dist/assets/web/dist/assets/{java-BbZjTNgf.js → java-dugWx3wi.js} +1 -1
- package/dist/assets/web/dist/assets/{javascript-DXCdqcZZ.js → javascript-CK4GC4JO.js} +1 -1
- package/dist/assets/web/dist/assets/{jinja-pDlsjTVp.js → jinja-DrLKIyKG.js} +1 -1
- package/dist/assets/web/dist/assets/{jison-DsXDk66e.js → jison-CS1Th-TO.js} +1 -1
- package/dist/assets/web/dist/assets/{json-tCxXfRgG.js → json-CkBqdVys.js} +1 -1
- package/dist/assets/web/dist/assets/{jsx-B0ZYkCmi.js → jsx-CCxdg7n3.js} +1 -1
- package/dist/assets/web/dist/assets/{julia-BLzzeI6x.js → julia-BalKW2hA.js} +1 -1
- package/dist/assets/web/dist/assets/{just-DvD2_60c.js → just-BKw1G40D.js} +1 -1
- package/dist/assets/web/dist/assets/{latex-511zn3h7.js → latex-Dfad6ZN-.js} +1 -1
- package/dist/assets/web/dist/assets/{liquid-BD0BkMox.js → liquid-f_CMGBvd.js} +1 -1
- package/dist/assets/web/dist/assets/{indexFormat-B-pd4r7Y.js → localIso-CNA-bhS-.js} +1 -1
- package/dist/assets/web/dist/assets/{lua-NFicKm6N.js → lua-BcMK_uKM.js} +1 -1
- package/dist/assets/web/dist/assets/main-B77vAZVg.css +1 -0
- package/dist/assets/web/dist/assets/main-DR8iKpNt.js +28 -0
- package/dist/assets/web/dist/assets/{marko-1-G_nEXK.js → marko-B0oNLx2i.js} +1 -1
- package/dist/assets/web/dist/assets/{mdc-DUe3AQLp.js → mdc-Bc_MmIyq.js} +1 -1
- package/dist/assets/web/dist/assets/{nginx-Dg379_bA.js → nginx-Cmxv8RIt.js} +1 -1
- package/dist/assets/web/dist/assets/{nim-BnFq97ZO.js → nim-BtgRfaY0.js} +1 -1
- package/dist/assets/web/dist/assets/{org-F0uiwGvq.js → org-Dl9EzlHh.js} +1 -1
- package/dist/assets/web/dist/assets/{perl-B0euczkl.js → perl-BmF7x7BB.js} +1 -1
- package/dist/assets/web/dist/assets/{php-DkL3k_n7.js → php-rH6CVptQ.js} +1 -1
- package/dist/assets/web/dist/assets/{pug-L_OjjZP4.js → pug-CIN5Ccck.js} +1 -1
- package/dist/assets/web/dist/assets/{qml-BMjB00Zz.js → qml-BpLS3RFr.js} +1 -1
- package/dist/assets/web/dist/assets/{r-DoeLdnqR.js → r-BUMF3B-H.js} +1 -1
- package/dist/assets/web/dist/assets/{razor-BzYNkAWy.js → razor-B7sTBPdT.js} +1 -1
- package/dist/assets/web/dist/assets/{regexp-CPmElMk3.js → regexp-h2SvwrsJ.js} +1 -1
- package/dist/assets/web/dist/assets/residentsModel-VSJEepL1.js +1 -0
- package/dist/assets/web/dist/assets/{rst-DrsjgfI7.js → rst-DLOWEsD2.js} +1 -1
- package/dist/assets/web/dist/assets/{ruby-cB42ppvx.js → ruby-Cl_-I4k3.js} +1 -1
- package/dist/assets/web/dist/assets/{sas-R5Y3NM_I.js → sas-Cm4M-BvV.js} +1 -1
- package/dist/assets/web/dist/assets/{scss-C-zWxV9s.js → scss-CEFCEQUF.js} +1 -1
- package/dist/assets/web/dist/assets/{shellscript-oJF96aAU.js → shellscript-CE6zb5eS.js} +1 -1
- package/dist/assets/web/dist/assets/{shellsession-CznECKyi.js → shellsession-B4fDVzrM.js} +1 -1
- package/dist/assets/web/dist/assets/{soy-wjHLSRag.js → soy-DwXCMpjE.js} +1 -1
- package/dist/assets/web/dist/assets/{sql-C_BM8IOW.js → sql-DrnAKnyD.js} +1 -1
- package/dist/assets/web/dist/assets/{stata-D31HO8aQ.js → stata-BzlwPWD6.js} +1 -1
- package/dist/assets/web/dist/assets/{surrealql-CKCLyplA.js → surrealql-CeUieZXd.js} +1 -1
- package/dist/assets/web/dist/assets/{svelte-3geWk2iu.js → svelte-BERIpIKR.js} +1 -1
- package/dist/assets/web/dist/assets/{templ-DiJr_hTD.js → templ-6FSuUDwg.js} +1 -1
- package/dist/assets/web/dist/assets/{tex-BEJGWyeF.js → tex-SWCyMwNX.js} +1 -1
- package/dist/assets/web/dist/assets/{ts-tags-C8Mzdpxx.js → ts-tags-VWj7gWgY.js} +1 -1
- package/dist/assets/web/dist/assets/{tsx-UQiSm_p3.js → tsx-B6dzfL8L.js} +1 -1
- package/dist/assets/web/dist/assets/{twig-_Pvzkeo9.js → twig-BDXsr482.js} +1 -1
- package/dist/assets/web/dist/assets/{typescript-BWMkINKK.js → typescript-s100Dt8y.js} +1 -1
- package/dist/assets/web/dist/assets/{typst-Bdg9m-e7.js → typst-WfyEAUdt.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-CDZrrAZ8.js → vue-CkXOOJ-6.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-html-tAlpNhfN.js → vue-html-CEpzdcau.js} +1 -1
- package/dist/assets/web/dist/assets/{vue-vine-C27UF_rh.js → vue-vine-D9dsfiFA.js} +1 -1
- package/dist/assets/web/dist/assets/{xml-CxeDr9Zh.js → xml-iBDofbJq.js} +1 -1
- package/dist/assets/web/dist/assets/{xsl-Bpi0uUnM.js → xsl-CqbJF4hD.js} +1 -1
- package/dist/assets/web/dist/assets/{yaml-BtUgZ1GN.js → yaml-D9i6OxYe.js} +1 -1
- package/dist/cli.js +2336 -1192
- package/package.json +1 -1
- package/dist/assets/web/dist/assets/ResidentDetailPage-C_WNBUao.js +0 -1
- package/dist/assets/web/dist/assets/ResidentsIndexPage-D1sTyaZ_.js +0 -1
- package/dist/assets/web/dist/assets/RunFoldRow-C_bcTSjA.js +0 -5
- package/dist/assets/web/dist/assets/RunRoutePage-BWTi_6PG.js +0 -10
- package/dist/assets/web/dist/assets/RunsIndexPage-H6GkSv0a.js +0 -1
- package/dist/assets/web/dist/assets/ScheduledPage-BLcPzm1S.js +0 -1
- package/dist/assets/web/dist/assets/UnitRoutePage-CIhTEwN7.js +0 -1
- package/dist/assets/web/dist/assets/indexRow-Bnj883ii.js +0 -1
- package/dist/assets/web/dist/assets/localIso-L06jV29p.js +0 -1
- package/dist/assets/web/dist/assets/main-CRtlGxRm.js +0 -28
- package/dist/assets/web/dist/assets/main-Dm11o0hc.css +0 -1
- package/dist/assets/web/dist/assets/residentDiskBudget-BMBKlYRH.js +0 -1
|
@@ -48,6 +48,11 @@ const FLEET_BUSY_PATTERNS: readonly RegExp[] = [
|
|
|
48
48
|
/^Failed to create session: 503\b/i,
|
|
49
49
|
/no container instance (?:that can be provided|available)/i,
|
|
50
50
|
/\bCONTAINER_UNAVAILABLE\b/,
|
|
51
|
+
// The platform's wording since the 0.13 line ("… Try again later, or try
|
|
52
|
+
// configuring a higher value for max_instances"): the 0.13 SDK's own warm
|
|
53
|
+
// pool matches on this exact phrase. Seen live passing through as a plain
|
|
54
|
+
// in-body error and ending two reviews in under a minute each.
|
|
55
|
+
/Maximum number of running container instances exceeded/i,
|
|
51
56
|
];
|
|
52
57
|
|
|
53
58
|
export function isFleetBusy(message: string): boolean {
|
|
@@ -120,6 +125,78 @@ export function fleetBusyExhaustedMessage(waitedMs: number): string {
|
|
|
120
125
|
);
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// A container that is still starting (docs/reference/specs/execution.md item 23).
|
|
130
|
+
//
|
|
131
|
+
// Why this exists: a thread's first request finds no running container, and
|
|
132
|
+
// the SDK's first exec then carries the whole start — the platform's instance
|
|
133
|
+
// grant (its default wait 30 s), the image pull on a machine that has not seen
|
|
134
|
+
// this image, the microVM boot and the runtime's port (90 s) — before the
|
|
135
|
+
// command runs. The executor's per-send deadline for a 60 s command is 90 s,
|
|
136
|
+
// so every fresh-sandbox run died with "gave no answer within 90s" while its
|
|
137
|
+
// container came up a minute later and sat idle. The Worker now names the
|
|
138
|
+
// condition instead: it starts the container in the background and answers
|
|
139
|
+
// `sandbox-starting` at once, the executor waits on the token exactly as it
|
|
140
|
+
// waits on `fleet-busy` — the identical request re-sent, nothing ran — under a
|
|
141
|
+
// start budget of its own, and the command runs once the container is up.
|
|
142
|
+
|
|
143
|
+
/** The machine token the executor waits on, beside `fleet-busy`. */
|
|
144
|
+
export const SANDBOX_STARTING_REASON = "sandbox-starting" as const;
|
|
145
|
+
|
|
146
|
+
/** What the token means, in the words the model and the operator see. */
|
|
147
|
+
export const SANDBOX_STARTING_EXPLANATION =
|
|
148
|
+
"the thread's sandbox container is starting (image pull, boot, runtime) — nothing ran yet; the request is re-sent once it is up";
|
|
149
|
+
|
|
150
|
+
/** The executor waits at most this long for a container to start, whatever
|
|
151
|
+
* the command's own budget: the platform's own start allowances (30 s for an
|
|
152
|
+
* instance, 90 s for the port) plus a slow image pull fit inside it, and a
|
|
153
|
+
* 60 s command is never killed by a two-minute start it did not cause. */
|
|
154
|
+
export const SANDBOX_START_WAIT_MAX_MS = 5 * 60_000;
|
|
155
|
+
|
|
156
|
+
/** Backoff between re-sends while a container starts: 5 s, 10 s, then 15 s.
|
|
157
|
+
* A start takes tens of seconds, not minutes, so the poll is denser than the
|
|
158
|
+
* fleet wait's and a ready container is used within 15 s of coming up. */
|
|
159
|
+
export const SANDBOX_START_BACKOFF_MS: readonly number[] = [5_000, 10_000, 15_000];
|
|
160
|
+
|
|
161
|
+
/** The reasons whose answers the executor re-sends after a wait. Every other
|
|
162
|
+
* `reason` — or none — is an ordinary failure after one send. */
|
|
163
|
+
export type WaitReason = typeof FLEET_BUSY_REASON | typeof SANDBOX_STARTING_REASON;
|
|
164
|
+
|
|
165
|
+
export function isWaitReason(reason: unknown): reason is WaitReason {
|
|
166
|
+
return reason === FLEET_BUSY_REASON || reason === SANDBOX_STARTING_REASON;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The Worker's answer on /read and /write (sent as HTTP 503) while the
|
|
170
|
+
* container starts: the token, and the start's own phase as the cause. */
|
|
171
|
+
export function sandboxStartingAnswer(cause: string): { error: string; reason: typeof SANDBOX_STARTING_REASON } {
|
|
172
|
+
return {
|
|
173
|
+
error: `${SANDBOX_STARTING_REASON}: ${SANDBOX_STARTING_EXPLANATION} (${cause})`,
|
|
174
|
+
reason: SANDBOX_STARTING_REASON,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The Worker's answer on /exec, in-body under the streamed HTTP 200 in the
|
|
179
|
+
* item-3 dual shape, like `fleetBusyExecAnswer`. */
|
|
180
|
+
export function sandboxStartingExecAnswer(cause: string): {
|
|
181
|
+
error: string;
|
|
182
|
+
reason: typeof SANDBOX_STARTING_REASON;
|
|
183
|
+
stdout: "";
|
|
184
|
+
stderr: string;
|
|
185
|
+
exitCode: 127;
|
|
186
|
+
} {
|
|
187
|
+
const { error, reason } = sandboxStartingAnswer(cause);
|
|
188
|
+
return { error, reason, stdout: "", stderr: error, exitCode: 127 };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The message `ExecCapacityError` carries when a container did not start
|
|
192
|
+
* inside the start budget: the wait, and what to do. */
|
|
193
|
+
export function startWaitExhaustedMessage(waitedMs: number): string {
|
|
194
|
+
return (
|
|
195
|
+
`sandbox not ready — the thread's container did not finish starting within ${Math.round(waitedMs / 1000)}s; ` +
|
|
196
|
+
"try again in a few minutes"
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
123
200
|
/** The text the Worker carries in-body for a thrown value: the SDK's own
|
|
124
201
|
* message when it has one, else a sentence that says the SDK gave none —
|
|
125
202
|
* naming the error's name and code, and the one condition known to produce
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// The per-thread sandbox's idle deadline, decided by the Durable Object from
|
|
2
|
+
// the one fact it owns (docs/reference/specs/execution.md item 22): when it
|
|
3
|
+
// last served a request. Deliberately free of node: imports so wrangler can
|
|
4
|
+
// bundle it into the sandbox Worker, like sandboxErrors.ts and
|
|
5
|
+
// sandboxLifecycle.ts.
|
|
6
|
+
//
|
|
7
|
+
// Why this exists: on the 0.13 SDK line the Container's `sleepAfter` is no
|
|
8
|
+
// longer a deadline. At expiry the SDK asks its runtime whether any tracked
|
|
9
|
+
// process or terminal is still active and, if so — or if the probe fails at
|
|
10
|
+
// all — renews the timeout instead of stopping. A detached pi holds its
|
|
11
|
+
// command's stdio open, a bot deploy mid-run orphans that pi until the same
|
|
12
|
+
// thread's next run, and the container is awake for good: twenty-five
|
|
13
|
+
// sandboxes were found running 10–16 h after their last request against a
|
|
14
|
+
// 5-minute sleepAfter with three runs in flight, and the fleet answered every
|
|
15
|
+
// new thread "Maximum number of running container instances exceeded". The
|
|
16
|
+
// guard below makes the deadline ours again: served-time is recorded on every
|
|
17
|
+
// request, a sweep the Durable Object schedules for itself checks it once a
|
|
18
|
+
// minute (and the SDK's own expiry hook is answered by the same verdict), and
|
|
19
|
+
// a container past the window is destroyed — through the SDK's clean teardown
|
|
20
|
+
// when that finishes in time, by the platform's own kill when it does not.
|
|
21
|
+
// Nothing inside the container can extend its life; only a request can.
|
|
22
|
+
|
|
23
|
+
import { BASH_TIMEOUT_MAX_MS } from "./bashTimeout.js";
|
|
24
|
+
|
|
25
|
+
/** The idle window in milliseconds — the twin of `SANDBOX_SLEEP_AFTER` ("5m",
|
|
26
|
+
* sandboxLifecycle.ts), which stays the SDK's own setting so its alarm loop
|
|
27
|
+
* still calls `onActivityExpired` on this cadence. A test holds the two
|
|
28
|
+
* together. */
|
|
29
|
+
export const SANDBOX_SLEEP_AFTER_MS = 5 * 60_000;
|
|
30
|
+
|
|
31
|
+
/** The guard's own cadence: a scheduled callback the Durable Object re-arms
|
|
32
|
+
* after each run while there is a container to guard. Independent of the
|
|
33
|
+
* SDK's activity renewals, so a deadline is met within a minute of passing
|
|
34
|
+
* even when the SDK's busy poll renews its timeout every second. */
|
|
35
|
+
export const IDLE_SWEEP_INTERVAL_MS = 60_000;
|
|
36
|
+
|
|
37
|
+
/** A request in flight this long is stuck, not service: the longest command
|
|
38
|
+
* `/exec` admits (`BASH_TIMEOUT_MAX_MS`, 20 min) plus the SDK backstop and
|
|
39
|
+
* output-wait margins with room to spare. Requests are measured one by one
|
|
40
|
+
* (each carries its own start), so an overlapping chain of short polls beside
|
|
41
|
+
* a long command is never read as one long request. */
|
|
42
|
+
export const INFLIGHT_STUCK_MS = BASH_TIMEOUT_MAX_MS + 10 * 60_000;
|
|
43
|
+
|
|
44
|
+
/** How often the served-time reaches Durable Object storage: the in-memory
|
|
45
|
+
* fact is exact while the object lives, storage is the baseline for the next
|
|
46
|
+
* wake, and the pi harness polls every 750 ms — one write per poll would be
|
|
47
|
+
* the loudest thing the Worker does. */
|
|
48
|
+
export const PERSIST_EVERY_MS = 10_000;
|
|
49
|
+
|
|
50
|
+
/** How long the SDK's clean `destroy()` may take before the platform's own
|
|
51
|
+
* kill ends the container regardless. The SDK bounds its runtime cleanup at
|
|
52
|
+
* 30 s; a teardown that has not finished twice that is not going to. */
|
|
53
|
+
export const DESTROY_GRACE_MS = 60_000;
|
|
54
|
+
|
|
55
|
+
/** The facts the verdict is drawn from. `inflight` holds the start time of
|
|
56
|
+
* every request being served right now — a list, not a count, so the oldest
|
|
57
|
+
* LIVE request decides "stuck" and a finished one stops counting. */
|
|
58
|
+
export interface IdleLedger {
|
|
59
|
+
lastServedAt: number;
|
|
60
|
+
inflight: number[];
|
|
61
|
+
lastPersistedAt: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function newIdleLedger(now: number): IdleLedger {
|
|
65
|
+
return { lastServedAt: now, inflight: [], lastPersistedAt: now };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type IdleVerdict =
|
|
69
|
+
| { action: "destroy"; why: "idle" | "stuck"; idleMs: number }
|
|
70
|
+
| { action: "keep"; why: "warm" | "serving"; recheckInMs: number };
|
|
71
|
+
|
|
72
|
+
/** The decision, pure: destroy when nothing is in flight and the last request
|
|
73
|
+
* finished a full window ago, or when the oldest request in flight has been
|
|
74
|
+
* there longer than any command may run; keep otherwise. A clock that went
|
|
75
|
+
* backwards reads as a fresh request, never as an idle container. */
|
|
76
|
+
export function idleVerdict(ledger: IdleLedger, now: number, sleepAfterMs = SANDBOX_SLEEP_AFTER_MS): IdleVerdict {
|
|
77
|
+
if (ledger.inflight.length > 0) {
|
|
78
|
+
const oldest = Math.min(...ledger.inflight);
|
|
79
|
+
const inflightMs = now - oldest;
|
|
80
|
+
if (inflightMs >= INFLIGHT_STUCK_MS) return { action: "destroy", why: "stuck", idleMs: inflightMs };
|
|
81
|
+
return { action: "keep", why: "serving", recheckInMs: IDLE_SWEEP_INTERVAL_MS };
|
|
82
|
+
}
|
|
83
|
+
const idleMs = now - ledger.lastServedAt;
|
|
84
|
+
if (idleMs >= sleepAfterMs) return { action: "destroy", why: "idle", idleMs };
|
|
85
|
+
return { action: "keep", why: "warm", recheckInMs: sleepAfterMs - idleMs };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** What the guard needs from the Durable Object, as plain functions so the
|
|
89
|
+
* whole decision — arming, counting, destroying, forcing — is exercised
|
|
90
|
+
* against a fake in tests and the Worker's class only forwards. */
|
|
91
|
+
export interface IdleGuardHost {
|
|
92
|
+
now(): number;
|
|
93
|
+
/** `ctx.container?.running`: false when the platform says stopped, true when
|
|
94
|
+
* running, undefined when the object cannot tell — guarded like running. */
|
|
95
|
+
containerRunning(): boolean | undefined;
|
|
96
|
+
/** Whether a sweep callback is already scheduled (the SDK's schedule table). */
|
|
97
|
+
sweepScheduled(): Promise<boolean>;
|
|
98
|
+
scheduleSweep(delayMs: number): Promise<void>;
|
|
99
|
+
/** The SDK's clean teardown (`Sandbox.destroy()`): sessions closed, the
|
|
100
|
+
* container SIGKILLed at the end. May hang or throw; the guard bounds it. */
|
|
101
|
+
destroySandbox(): Promise<void>;
|
|
102
|
+
/** The platform primitive (`ctx.container.destroy()`): the microVM is gone. */
|
|
103
|
+
killContainer(): Promise<void>;
|
|
104
|
+
loadLastServedAt(): Promise<number | undefined>;
|
|
105
|
+
saveLastServedAt(at: number): Promise<void>;
|
|
106
|
+
log(event: Record<string, unknown>): void;
|
|
107
|
+
wait(ms: number): Promise<void>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type IdleStopSource = "sweep" | "sdk-expiry";
|
|
111
|
+
|
|
112
|
+
export class IdleGuard {
|
|
113
|
+
readonly ledger: IdleLedger;
|
|
114
|
+
private armed = false;
|
|
115
|
+
private destroying: Promise<void> | null = null;
|
|
116
|
+
|
|
117
|
+
constructor(private readonly host: IdleGuardHost) {
|
|
118
|
+
this.ledger = newIdleLedger(host.now());
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** On every Durable Object wake (its constructor): the stored served-time is
|
|
122
|
+
* the baseline when there is one — a container found awake with no record
|
|
123
|
+
* is idle from now, so a fleet leaked before this code arrived is gone one
|
|
124
|
+
* window after the deploy — and one sweep is armed when a container may be
|
|
125
|
+
* running and none is scheduled. */
|
|
126
|
+
async wake(): Promise<void> {
|
|
127
|
+
const stored = await this.host.loadLastServedAt();
|
|
128
|
+
if (stored !== undefined) {
|
|
129
|
+
this.ledger.lastServedAt = stored;
|
|
130
|
+
this.ledger.lastPersistedAt = stored;
|
|
131
|
+
}
|
|
132
|
+
if (this.guarding()) await this.arm();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Every request the object serves runs inside this: counted in flight from
|
|
136
|
+
* its start (so a live command is never idle), recorded at its finish
|
|
137
|
+
* (success or failure alike), persisted on the persist cadence. */
|
|
138
|
+
async served<T>(op: () => Promise<T>): Promise<T> {
|
|
139
|
+
const startedAt = this.host.now();
|
|
140
|
+
this.ledger.inflight.push(startedAt);
|
|
141
|
+
try {
|
|
142
|
+
await this.arm();
|
|
143
|
+
return await op();
|
|
144
|
+
} finally {
|
|
145
|
+
const i = this.ledger.inflight.indexOf(startedAt);
|
|
146
|
+
if (i >= 0) this.ledger.inflight.splice(i, 1);
|
|
147
|
+
const now = this.host.now();
|
|
148
|
+
this.ledger.lastServedAt = now;
|
|
149
|
+
if (now - this.ledger.lastPersistedAt >= PERSIST_EVERY_MS) {
|
|
150
|
+
this.ledger.lastPersistedAt = now;
|
|
151
|
+
try {
|
|
152
|
+
await this.host.saveLastServedAt(now);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
this.host.log({ event: "sandbox.idle-ledger.persist-failed", error: String(err) });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The scheduled callback. The SDK deletes a schedule row once it has run,
|
|
161
|
+
* so the guard re-arms itself here while there is a container to guard;
|
|
162
|
+
* with none (destroyed, or never started), the next request arms it. */
|
|
163
|
+
async sweep(): Promise<void> {
|
|
164
|
+
this.armed = false;
|
|
165
|
+
await this.enforce("sweep");
|
|
166
|
+
if (this.guarding()) await this.arm();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The SDK's `onActivityExpired`, answered by the same verdict — its
|
|
170
|
+
* process and terminal probes never decide. */
|
|
171
|
+
async expired(): Promise<void> {
|
|
172
|
+
await this.enforce("sdk-expiry");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private guarding(): boolean {
|
|
176
|
+
return this.host.containerRunning() !== false || this.ledger.inflight.length > 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async arm(): Promise<void> {
|
|
180
|
+
if (this.armed) return;
|
|
181
|
+
if (!(await this.host.sweepScheduled())) await this.host.scheduleSweep(IDLE_SWEEP_INTERVAL_MS);
|
|
182
|
+
this.armed = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async enforce(source: IdleStopSource): Promise<void> {
|
|
186
|
+
if (this.destroying) return this.destroying;
|
|
187
|
+
if (!this.guarding()) return;
|
|
188
|
+
const verdict = idleVerdict(this.ledger, this.host.now());
|
|
189
|
+
if (verdict.action === "keep") return;
|
|
190
|
+
this.destroying = this.destroy(verdict, source).finally(() => {
|
|
191
|
+
this.destroying = null;
|
|
192
|
+
});
|
|
193
|
+
return this.destroying;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The SDK's clean destroy, bounded; then the platform's kill unless the
|
|
197
|
+
* container is known stopped. The guarantee lives in the second step. */
|
|
198
|
+
private async destroy(verdict: Extract<IdleVerdict, { action: "destroy" }>, source: IdleStopSource): Promise<void> {
|
|
199
|
+
const base = { why: verdict.why, idleMs: verdict.idleMs, source, sleepAfterMs: SANDBOX_SLEEP_AFTER_MS };
|
|
200
|
+
this.host.log({ event: "sandbox.idle-stop", ...base });
|
|
201
|
+
let outcome: "done" | "timeout" | "failed";
|
|
202
|
+
let error: string | undefined;
|
|
203
|
+
try {
|
|
204
|
+
outcome = await Promise.race([
|
|
205
|
+
this.host.destroySandbox().then(() => "done" as const),
|
|
206
|
+
this.host.wait(DESTROY_GRACE_MS).then(() => "timeout" as const),
|
|
207
|
+
]);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
outcome = "failed";
|
|
210
|
+
error = String(err);
|
|
211
|
+
}
|
|
212
|
+
if (outcome === "done" && this.host.containerRunning() === false) return;
|
|
213
|
+
this.host.log({ event: "sandbox.idle-stop.forced", ...base, outcome, ...(error ? { error } : {}) });
|
|
214
|
+
try {
|
|
215
|
+
await this.host.killContainer();
|
|
216
|
+
} catch (err) {
|
|
217
|
+
this.host.log({ event: "sandbox.idle-stop.kill-failed", ...base, error: String(err) });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// The start gate of a thread's sandbox (docs/reference/specs/execution.md
|
|
2
|
+
// item 23): the Durable Object's decision, on every request, between running
|
|
3
|
+
// the operation and answering `sandbox-starting`. Deliberately free of node:
|
|
4
|
+
// imports so wrangler can bundle it into the sandbox Worker, like
|
|
5
|
+
// sandboxErrors.ts and sandboxIdle.ts.
|
|
6
|
+
//
|
|
7
|
+
// Why: a thread's first request finds no running container, and the SDK's
|
|
8
|
+
// first exec then carries the whole start — the platform's instance grant,
|
|
9
|
+
// the image pull, the microVM boot, the runtime's port — before the command
|
|
10
|
+
// runs; the executor's per-send deadline for a 60 s command is 90 s, so every
|
|
11
|
+
// fresh-sandbox run died with "gave no answer within 90s" while its container
|
|
12
|
+
// came up a minute later. The gate starts the container in the background
|
|
13
|
+
// through a warm-up the host provides, answers the named token at once, and
|
|
14
|
+
// lets the operation through only once the container is up. A warm-up that
|
|
15
|
+
// fails hands its error to the next request, so a full fleet or a silent
|
|
16
|
+
// runtime keeps its own name (item 14, item 9) — the gate never swallows it.
|
|
17
|
+
|
|
18
|
+
/** What the gate needs from the Durable Object. */
|
|
19
|
+
export interface StartGateHost {
|
|
20
|
+
/** `ctx.container?.running`: true once the platform runs the container,
|
|
21
|
+
* false when it is stopped, undefined when there is no container binding
|
|
22
|
+
* (treated as not running: the warm-up will say what is wrong). */
|
|
23
|
+
containerRunning(): boolean | undefined;
|
|
24
|
+
/** Start the container and wait for its runtime: one trivial command
|
|
25
|
+
* through the SDK, which does the start itself. Resolves when the runtime
|
|
26
|
+
* answered; rejects with the SDK's own error otherwise. */
|
|
27
|
+
warmUp(): Promise<void>;
|
|
28
|
+
now(): number;
|
|
29
|
+
log(event: Record<string, unknown>): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The gate's answer when the operation cannot run yet: the phase the start
|
|
33
|
+
* is in, in words, for the answer's cause. */
|
|
34
|
+
export type StartingCause = "container not running; starting it" | "container starting";
|
|
35
|
+
|
|
36
|
+
export class StartGate {
|
|
37
|
+
private starting: Promise<void> | null = null;
|
|
38
|
+
private startedAt = 0;
|
|
39
|
+
private failure: { error: unknown } | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(private readonly host: StartGateHost) {}
|
|
42
|
+
|
|
43
|
+
/** Run `op` when the container is up; otherwise answer `starting(cause)`
|
|
44
|
+
* at once — beginning the warm-up on the first such request — and let the
|
|
45
|
+
* executor's wait re-send. A warm-up that failed is thrown here once, on
|
|
46
|
+
* the next request, so the caller classifies it as it would any SDK error
|
|
47
|
+
* (a full fleet, a silent control port); the request after that starts a
|
|
48
|
+
* fresh warm-up if the container is still not running. */
|
|
49
|
+
async through<T, S>(op: () => Promise<T>, starting: (cause: StartingCause) => S): Promise<T | S> {
|
|
50
|
+
if (this.failure) {
|
|
51
|
+
const { error } = this.failure;
|
|
52
|
+
this.failure = null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (this.starting) return starting("container starting");
|
|
56
|
+
if (this.host.containerRunning() !== true) {
|
|
57
|
+
this.begin();
|
|
58
|
+
return starting("container not running; starting it");
|
|
59
|
+
}
|
|
60
|
+
return op();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether a warm-up is in flight — for the wiring test and the card. */
|
|
64
|
+
get isStarting(): boolean {
|
|
65
|
+
return this.starting !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private begin(): void {
|
|
69
|
+
this.startedAt = this.host.now();
|
|
70
|
+
this.host.log({ event: "sandbox.starting" });
|
|
71
|
+
this.starting = this.host
|
|
72
|
+
.warmUp()
|
|
73
|
+
.then(
|
|
74
|
+
() => {
|
|
75
|
+
this.host.log({ event: "sandbox.started", durationMs: this.host.now() - this.startedAt });
|
|
76
|
+
},
|
|
77
|
+
(error: unknown) => {
|
|
78
|
+
this.host.log({
|
|
79
|
+
event: "sandbox.start-failed",
|
|
80
|
+
durationMs: this.host.now() - this.startedAt,
|
|
81
|
+
error: String(error),
|
|
82
|
+
});
|
|
83
|
+
this.failure = { error };
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
.finally(() => {
|
|
87
|
+
this.starting = null;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|