@coreplane/switchboard 1.201.0 → 1.202.1
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/README.md +1 -1
- package/bin/switchboard.js +26 -0
- package/dist/assets/deploy/cloudflare-resident/refresh.ts +238 -0
- package/dist/assets/deploy/cloudflare-resident/shared.ts +80 -0
- package/dist/assets/deploy/cloudflare-resident/tsconfig.json +1 -1
- package/dist/assets/deploy/cloudflare-resident/worker.ts +67 -285
- package/dist/assets/deploy/cloudflare-resident/wrangler.template.jsonc +2 -1
- package/dist/assets/package-lock.json +481 -4
- package/dist/assets/package.json +1 -1
- package/dist/assets/project.json +1 -1
- package/dist/assets/source.json +3 -3
- package/dist/assets/src/core/authz/policy.ts +3 -0
- package/dist/assets/src/execution/residentRefresh.ts +29 -0
- package/dist/assets/web/dist/.vite/manifest.json +43 -43
- package/dist/assets/web/dist/assets/{AppShell-CjDyTiip.js → AppShell-CJLPO_aI.js} +1 -1
- package/dist/assets/web/dist/assets/{CostsPage-CMAh3G74.js → CostsPage-C6ZvnAmH.js} +1 -1
- package/dist/assets/web/dist/assets/{NotFoundPage-CbEKm0tY.js → NotFoundPage-BdLcY60r.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentDetailPage-CcfmOAIP.js → ResidentDetailPage-hXWrr8T6.js} +1 -1
- package/dist/assets/web/dist/assets/{ResidentsIndexPage-9Z5jcteM.js → ResidentsIndexPage-YLu4JwxJ.js} +1 -1
- package/dist/assets/web/dist/assets/{RunRoutePage-Cdimh4e0.js → RunRoutePage-BDhHRJVO.js} +1 -1
- package/dist/assets/web/dist/assets/{RunsIndexPage-CvgDXrO4.js → RunsIndexPage-D7zZ5a0l.js} +1 -1
- package/dist/assets/web/dist/assets/{RunsTabs-rEjVQG6H.js → RunsTabs-BOUlSa2W.js} +1 -1
- package/dist/assets/web/dist/assets/{ScheduledPage-C5bG9qvd.js → ScheduledPage-BciOQcC-.js} +1 -1
- package/dist/assets/web/dist/assets/{StatusDot-D_IPK3WP.js → StatusDot-CTDLBv92.js} +1 -1
- package/dist/assets/web/dist/assets/{Tooltip-JlZp3OVC.js → Tooltip-DD2v9Gxx.js} +1 -1
- package/dist/assets/web/dist/assets/{favicon-DZDjc8ab.js → favicon-C4Q8cRsP.js} +1 -1
- package/dist/assets/web/dist/assets/{main-BKKzFAnK.js → main-DrSlUcMg.js} +2 -2
- package/dist/cli.js +629 -207
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<img src="https://raw.githubusercontent.com/coreplanelabs/switchboard/HEAD/docs/public/logo-light.svg" width="56" height="56" alt="">
|
|
4
4
|
</picture>
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Switchboard
|
|
7
7
|
|
|
8
8
|
[](https://github.com/coreplanelabs/switchboard/actions/workflows/ci.yml) [](https://scorecard.dev/viewer/?uri=github.com/coreplanelabs/switchboard) [](https://github.com/coreplanelabs/switchboard/releases/latest) [](https://github.com/coreplanelabs/switchboard/blob/HEAD/LICENSE)
|
|
9
9
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `switchboard` bin: this file hands the process to dist/cli.js, the bundle
|
|
3
|
+
// the build writes (build.mts; docs/reference/specs/packaging.md item 1). A
|
|
4
|
+
// committed entry rather than the bundle itself, because npm links a bin only
|
|
5
|
+
// when its target exists at install time, and the gitignored dist/ does not
|
|
6
|
+
// until the build runs: with dist/cli.js as the bin, `npx <the package>` inside
|
|
7
|
+
// the checkout — where npx prefers the workspace over the registry — found no
|
|
8
|
+
// link and died with `sh: switchboard: command not found`. The published package
|
|
9
|
+
// always carries the bundle, so there this is one extra module load; a checkout
|
|
10
|
+
// that has not built the package is told what to run.
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const bundle = new URL("../dist/cli.js", import.meta.url);
|
|
15
|
+
|
|
16
|
+
if (!existsSync(bundle)) {
|
|
17
|
+
console.error(
|
|
18
|
+
`switchboard: ${fileURLToPath(bundle)} is missing — the package is not built. Inside the checkout, npx runs this workspace, not the published package: run \`npm run build -w packages/switchboard\` first, or use the checkout's CLI, \`npm run cli -- <group> <verb> …\`.`,
|
|
19
|
+
);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// From here the bundle is the script: its entry claim (src/invokedAsScript.ts)
|
|
24
|
+
// and its usage spelling (`programName`, src/cli.ts) read argv[1].
|
|
25
|
+
process.argv[1] = fileURLToPath(bundle);
|
|
26
|
+
await import(bundle.href);
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// The refresh cycle as a Workflow instance (docs/reference/specs/resident-repos.md
|
|
2
|
+
// item 7): the `ResidentRefresh` entrypoint the Workflows binding names, the
|
|
3
|
+
// watchdog cron's instance-creation duty (`createRefreshInstance`) and the
|
|
4
|
+
// existence probe it confirms a duplicate-id refusal with. The steps' own work
|
|
5
|
+
// stays in worker.ts as the Durable Object's `refreshInstance*` methods: this
|
|
6
|
+
// module only sequences them through the DO stub. The entry re-exports the
|
|
7
|
+
// class (the binding resolves `class_name` against the entry module), and
|
|
8
|
+
// nothing here imports the entry at runtime — the shared pieces come from
|
|
9
|
+
// shared.ts, the entry's types `type`-only.
|
|
10
|
+
import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from "cloudflare:workers";
|
|
11
|
+
import { systemClock } from "../../src/core/trace/clock.js";
|
|
12
|
+
import { startAdoptedRoot } from "../../src/core/trace/workerTrace.js";
|
|
13
|
+
import {
|
|
14
|
+
REFRESH_STEP_RETRIES,
|
|
15
|
+
refreshInstanceId,
|
|
16
|
+
shouldCreateRefreshInstance,
|
|
17
|
+
stepTimeoutMs,
|
|
18
|
+
type RefreshRow,
|
|
19
|
+
} from "../../src/execution/residentInstanceId.js";
|
|
20
|
+
import { RESTORE_MAX_MS, type RefreshPlan } from "../../src/execution/residentRefresh.js";
|
|
21
|
+
import { residentText } from "../../src/execution/residentText.js";
|
|
22
|
+
import { graftResidentSteps } from "../../src/execution/residentTrace.js";
|
|
23
|
+
import {
|
|
24
|
+
DEPS_STEP_OVERHEAD_MS,
|
|
25
|
+
errMsg,
|
|
26
|
+
GIT_NETWORK_TIMEOUT_MS,
|
|
27
|
+
IDLE_REFRESH_INTERVAL_S,
|
|
28
|
+
R2_TRANSFER_TIMEOUT_MS,
|
|
29
|
+
REFRESH_BUILD_TIMEOUT_MS,
|
|
30
|
+
REFRESH_INSTALL_TIMEOUT_MS,
|
|
31
|
+
REFRESH_INTERVAL_S,
|
|
32
|
+
residentStub,
|
|
33
|
+
tracer,
|
|
34
|
+
traceSinks,
|
|
35
|
+
} from "./shared";
|
|
36
|
+
import type { Env, InstanceStepTrace } from "./worker";
|
|
37
|
+
|
|
38
|
+
/** What a refresh instance is created with: the resident it runs for. Every
|
|
39
|
+
* other input is read from the resident's rows at each step, never carried. */
|
|
40
|
+
export interface RefreshInstanceParams {
|
|
41
|
+
resource: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What the cron did about one resident's refresh instance this pass. */
|
|
45
|
+
export interface RefreshInstanceAction {
|
|
46
|
+
id: string;
|
|
47
|
+
action: "created" | "duplicate" | "skipped" | "failed";
|
|
48
|
+
why: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The refresh instance's step budgets: each `step.do` timeout is the DO
|
|
52
|
+
* method's own budget, capped at the engine's 30-minute step ceiling
|
|
53
|
+
* (`stepTimeoutMs`), so a step timeout and a command timeout agree. */
|
|
54
|
+
const REFRESH_FETCH_STEP_BUDGET_MS = RESTORE_MAX_MS + GIT_NETWORK_TIMEOUT_MS; // a wake's restore, then the fetch
|
|
55
|
+
const REFRESH_INSTALL_STEP_BUDGET_MS = REFRESH_INSTALL_TIMEOUT_MS + DEPS_STEP_OVERHEAD_MS; // the install's own lease
|
|
56
|
+
const REFRESH_BUILD_STEP_BUDGET_MS = GIT_NETWORK_TIMEOUT_MS + REFRESH_BUILD_TIMEOUT_MS; // the build's mutex lease
|
|
57
|
+
const REFRESH_SNAPSHOT_STEP_BUDGET_MS = R2_TRANSFER_TIMEOUT_MS + GIT_NETWORK_TIMEOUT_MS; // the archives, then the reclaim pass and the disk sample
|
|
58
|
+
|
|
59
|
+
/** Whether the engine knows an instance by this id, in any status. A missing
|
|
60
|
+
* id rejects on `get` or on `status`; either way the answer is false. */
|
|
61
|
+
async function refreshInstanceExists(env: Env, id: string): Promise<boolean> {
|
|
62
|
+
try {
|
|
63
|
+
await (await env.RESIDENT_REFRESH.get(id)).status();
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The cron's instance-creation duty for one resident (item 7). Only a
|
|
71
|
+
* `workflow` row gets an instance, at most one per ten-minute bucket, never
|
|
72
|
+
* while a cycle is live (`shouldCreateRefreshInstance`); the id is
|
|
73
|
+
* deterministic per resident and bucket, so a second firing in one bucket
|
|
74
|
+
* meets the engine's duplicate-id refusal, which is the expected no-op. A
|
|
75
|
+
* skipped live cycle and a duplicate are recorded on the row for `/status`.
|
|
76
|
+
* An `alarm` resident answers null: nothing here touches it. */
|
|
77
|
+
export async function createRefreshInstance(
|
|
78
|
+
env: Env,
|
|
79
|
+
stub: ReturnType<typeof residentStub>,
|
|
80
|
+
resource: string,
|
|
81
|
+
row: RefreshRow,
|
|
82
|
+
): Promise<RefreshInstanceAction | null> {
|
|
83
|
+
if (row.lifecycle !== "workflow") return null;
|
|
84
|
+
const now = systemClock();
|
|
85
|
+
const decision = shouldCreateRefreshInstance(row, now, {
|
|
86
|
+
intervalS: REFRESH_INTERVAL_S,
|
|
87
|
+
idleIntervalS: IDLE_REFRESH_INTERVAL_S,
|
|
88
|
+
});
|
|
89
|
+
const slug = resource.slice("repo:".length);
|
|
90
|
+
const slash = slug.indexOf("/");
|
|
91
|
+
const id = refreshInstanceId(slug.slice(0, slash), slug.slice(slash + 1), now);
|
|
92
|
+
if (!decision.create) {
|
|
93
|
+
if (decision.why === "mid-cycle" || decision.why === "running")
|
|
94
|
+
await stub.recordRefreshSkipped(id, now, decision.why);
|
|
95
|
+
return { id, action: "skipped", why: decision.why };
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
await env.RESIDENT_REFRESH.create({ id, params: { resource } });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
const message = errMsg(err);
|
|
101
|
+
// The engine refuses an id that names an instance still inside its
|
|
102
|
+
// retention, and the refusal carries no code — so the id is asked, not
|
|
103
|
+
// the wording: an instance that answers for it exists, and the refusal
|
|
104
|
+
// was the duplicate it looks like. Any other failure stays a failure.
|
|
105
|
+
if (await refreshInstanceExists(env, id)) {
|
|
106
|
+
await stub.recordRefreshSkipped(id, now, "duplicate");
|
|
107
|
+
return { id, action: "duplicate", why: "duplicate" };
|
|
108
|
+
}
|
|
109
|
+
console.error(`resident-watchdog: creating refresh instance ${id} failed — ${message}`);
|
|
110
|
+
return { id, action: "failed", why: residentText(message) };
|
|
111
|
+
}
|
|
112
|
+
await stub.recordRefreshInstance(id, now);
|
|
113
|
+
console.log(`resident-watchdog: created refresh instance ${id}`);
|
|
114
|
+
return { id, action: "created", why: decision.why };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** What an instance answers when it ends: small facts for the engine's record. */
|
|
118
|
+
interface RefreshInstanceSummary {
|
|
119
|
+
instance: string;
|
|
120
|
+
/** `ok`, or the word a gate or a failure ended the cycle with. */
|
|
121
|
+
outcome: string;
|
|
122
|
+
step: "fetch" | "install" | "build" | "snapshot";
|
|
123
|
+
action?: RefreshPlan["action"];
|
|
124
|
+
sha?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** One refresh cycle as one short Workflow instance: `fetch`, `install` (only
|
|
128
|
+
* when the plan moved the lockfile key), `build` (only when the branch
|
|
129
|
+
* moved), `snapshot` — each a `step.do` calling the resident's own step
|
|
130
|
+
* method through the DO stub, under the retry policy `REFRESH_STEP_RETRIES`
|
|
131
|
+
* (six attempts, thirty seconds apart, doubling: about 15.5 minutes, past
|
|
132
|
+
* the 3 to 10 minutes a resident Worker rollover takes to settle) and a
|
|
133
|
+
* timeout equal to the method's own budget (never above the engine's 30
|
|
134
|
+
* minutes). Inputs to a step are the event's `resource`, the instance id and
|
|
135
|
+
* previous steps' returns — refs, shas, a key, a path — never a payload and
|
|
136
|
+
* never a credential. A step killed from outside (the container replaced
|
|
137
|
+
* under it) throws and the engine retries it into the same idempotent
|
|
138
|
+
* method; a gate that ends the cycle (idle, a container restart) or a
|
|
139
|
+
* failure of the repository's own (recorded as `degraded`, the last snapshot
|
|
140
|
+
* still serving) ends the instance with that word, and the next cron firing
|
|
141
|
+
* creates the next one from the row's state. The instance runs one cycle
|
|
142
|
+
* and returns: it is created by the watchdog cron per resident and
|
|
143
|
+
* ten-minute bucket (`createRefreshInstance`), never a loop.
|
|
144
|
+
*
|
|
145
|
+
* The run is the cycle's root span, `resident.refresh` carrying the instance
|
|
146
|
+
* id (docs/reference/specs/tracing.md item 25), with every command a step ran
|
|
147
|
+
* grafted under it as a `resident.<step>` child — the same shape the alarm's
|
|
148
|
+
* root has. */
|
|
149
|
+
export class ResidentRefresh extends WorkflowEntrypoint<Env, RefreshInstanceParams> {
|
|
150
|
+
async run(
|
|
151
|
+
event: Readonly<WorkflowEvent<RefreshInstanceParams>>,
|
|
152
|
+
step: WorkflowStep,
|
|
153
|
+
): Promise<RefreshInstanceSummary> {
|
|
154
|
+
const { resource } = event.payload;
|
|
155
|
+
const instance = event.instanceId;
|
|
156
|
+
const stub = residentStub(this.env, resource);
|
|
157
|
+
const root = startAdoptedRoot(tracer, "resident.refresh", {
|
|
158
|
+
sinks: traceSinks,
|
|
159
|
+
startedAt: event.timestamp.getTime(),
|
|
160
|
+
attrs: { instanceId: instance },
|
|
161
|
+
});
|
|
162
|
+
const graft = (answer: InstanceStepTrace) =>
|
|
163
|
+
graftResidentSteps(answer.trace, {
|
|
164
|
+
parent: root,
|
|
165
|
+
prefix: "resident",
|
|
166
|
+
baseAt: answer.startedAt,
|
|
167
|
+
clipAt: systemClock(),
|
|
168
|
+
});
|
|
169
|
+
const retries = REFRESH_STEP_RETRIES;
|
|
170
|
+
/** The word a step that did not finish ends the instance with, and its outcome for the root. */
|
|
171
|
+
const ended = (
|
|
172
|
+
at: RefreshInstanceSummary["step"],
|
|
173
|
+
answer: { status: "stopped"; why: string } | { status: "failed"; reason: string },
|
|
174
|
+
): RefreshInstanceSummary => ({
|
|
175
|
+
instance,
|
|
176
|
+
outcome: answer.status === "stopped" ? answer.why : "failed",
|
|
177
|
+
step: at,
|
|
178
|
+
});
|
|
179
|
+
let summary: RefreshInstanceSummary | undefined;
|
|
180
|
+
try {
|
|
181
|
+
const fetched = await step.do("fetch", { retries, timeout: stepTimeoutMs(REFRESH_FETCH_STEP_BUDGET_MS) }, () =>
|
|
182
|
+
stub.refreshInstanceFetch({ resource, instance }),
|
|
183
|
+
);
|
|
184
|
+
graft(fetched);
|
|
185
|
+
if (fetched.status !== "done") return (summary = ended("fetch", fetched));
|
|
186
|
+
let depsEntry: string | null = null;
|
|
187
|
+
if (fetched.install) {
|
|
188
|
+
const installed = await step.do(
|
|
189
|
+
"install",
|
|
190
|
+
{ retries, timeout: stepTimeoutMs(REFRESH_INSTALL_STEP_BUDGET_MS) },
|
|
191
|
+
() => stub.refreshInstanceInstall({ resource, instance, sha: fetched.sha, lockfileKey: fetched.lockfileKey }),
|
|
192
|
+
);
|
|
193
|
+
graft(installed);
|
|
194
|
+
if (installed.status !== "done") return (summary = ended("install", installed));
|
|
195
|
+
depsEntry = installed.entry;
|
|
196
|
+
}
|
|
197
|
+
if (fetched.action !== "unchanged") {
|
|
198
|
+
const built = await step.do("build", { retries, timeout: stepTimeoutMs(REFRESH_BUILD_STEP_BUDGET_MS) }, () =>
|
|
199
|
+
stub.refreshInstanceBuild({
|
|
200
|
+
resource,
|
|
201
|
+
instance,
|
|
202
|
+
sha: fetched.sha,
|
|
203
|
+
factsSha: fetched.factsSha,
|
|
204
|
+
lockfileKey: fetched.lockfileKey,
|
|
205
|
+
depsEntry,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
graft(built);
|
|
209
|
+
if (built.status !== "done") return (summary = ended("build", built));
|
|
210
|
+
}
|
|
211
|
+
const snapped = await step.do(
|
|
212
|
+
"snapshot",
|
|
213
|
+
{ retries, timeout: stepTimeoutMs(REFRESH_SNAPSHOT_STEP_BUDGET_MS) },
|
|
214
|
+
() =>
|
|
215
|
+
stub.refreshInstanceSnapshot({
|
|
216
|
+
resource,
|
|
217
|
+
instance,
|
|
218
|
+
ref: fetched.ref,
|
|
219
|
+
sha: fetched.sha,
|
|
220
|
+
lockfileKey: fetched.lockfileKey,
|
|
221
|
+
action: fetched.action,
|
|
222
|
+
mintError: fetched.mintError,
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
graft(snapped);
|
|
226
|
+
if (snapped.status !== "done") return (summary = ended("snapshot", snapped));
|
|
227
|
+
return (summary = { instance, outcome: "ok", step: "snapshot", action: fetched.action, sha: fetched.sha });
|
|
228
|
+
} catch (err) {
|
|
229
|
+
// A step out of retries: the engine records the failed instance by id;
|
|
230
|
+
// the row keeps its last state and the next cron firing starts the next cycle.
|
|
231
|
+
root.fail(err);
|
|
232
|
+
throw err;
|
|
233
|
+
} finally {
|
|
234
|
+
const outcome = summary?.outcome ?? "error";
|
|
235
|
+
root.end(outcome === "ok" || (summary !== undefined && outcome !== "failed") ? "ok" : "error", { outcome });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// What the resident Worker's entry (worker.ts) and the refresh instance
|
|
2
|
+
// (refresh.ts) both read: the tunables the cycle's cadence and step budgets
|
|
3
|
+
// derive from, the two Durable Object stub factories, the Worker's one tracer
|
|
4
|
+
// with its log sink, and the error-message helper. The module exists so that
|
|
5
|
+
// refresh.ts never imports worker.ts at runtime — the entry imports refresh.ts
|
|
6
|
+
// for the class the Workflows binding names, and an import back would be a
|
|
7
|
+
// cycle at module evaluation. The entry's types (`Env`, the DO classes) are
|
|
8
|
+
// imported `type`-only here and in refresh.ts; those imports are erased before
|
|
9
|
+
// either module evaluates.
|
|
10
|
+
import { getSandbox } from "@cloudflare/sandbox";
|
|
11
|
+
import { systemClock } from "../../src/core/trace/clock.js";
|
|
12
|
+
import { createTracer } from "../../src/core/trace/tracer.js";
|
|
13
|
+
import { workerLogSink } from "../../src/core/trace/workerTrace.js";
|
|
14
|
+
import type { Env } from "./worker";
|
|
15
|
+
|
|
16
|
+
/** Container sleep window, passed to every getSandbox() for ResidentDO.
|
|
17
|
+
* Invariant: REFRESH_INTERVAL_S and the watchdog cron (wrangler.jsonc,
|
|
18
|
+
* every 10 minutes) MUST both stay SHORTER than this window, so a healthy
|
|
19
|
+
* resident is re-warmed before the platform can sleep it. Bump together. */
|
|
20
|
+
export const SLEEP_AFTER = "20m";
|
|
21
|
+
|
|
22
|
+
/** Refresh alarm cadence (seconds). Each resident DO self-reschedules this
|
|
23
|
+
* alarm (per-resident alarms own freshness; the sparse cron is only the
|
|
24
|
+
* watchdog); it doubles as the keep-warm heartbeat, so it must stay below
|
|
25
|
+
* SLEEP_AFTER. Matches the watchdog cron so a killed chain is re-armed
|
|
26
|
+
* within one refresh interval. */
|
|
27
|
+
export const REFRESH_INTERVAL_S = 600;
|
|
28
|
+
|
|
29
|
+
/** How far out an idle resident's cycle is parked (seconds): the alarm's idle
|
|
30
|
+
* re-arm (`IDLE_AFTER_S` in worker.ts decides idleness) and the cron's idle
|
|
31
|
+
* cadence, in whole ten-minute buckets since the last instance. */
|
|
32
|
+
export const IDLE_REFRESH_INTERVAL_S = 6 * 60 * 60;
|
|
33
|
+
|
|
34
|
+
/** Exec budgets. The DO alarm handler has a ~15-minute platform wall clock;
|
|
35
|
+
* every schedule callback's step budgets are chosen to fit under it. */
|
|
36
|
+
export const DEFAULT_EXEC_TIMEOUT_MS = 60_000;
|
|
37
|
+
export const GIT_NETWORK_TIMEOUT_MS = 5 * 60_000;
|
|
38
|
+
export const REFRESH_BUILD_TIMEOUT_MS = 5 * 60_000;
|
|
39
|
+
/** The refresh install budget. Twice the build's: a full `npm install` of the
|
|
40
|
+
* switchboard lockfile takes ~4 min on the resident's 1 vCPU when nothing
|
|
41
|
+
* else runs, and thread runs (tests, a review's greps) share that vCPU —
|
|
42
|
+
* under load it crosses 5 min several cycles in a row while the default
|
|
43
|
+
* branch keeps moving. A timed-out install is worse than a slow one: the cycle's whole
|
|
44
|
+
* budget is spent and the checkout is left without deps, so the next cycle
|
|
45
|
+
* starts the same install over. The cycle runs in the background (runs keep
|
|
46
|
+
* attaching to the last snapshot); the cost of a longer budget is a longer
|
|
47
|
+
* mirror-lock window, bounded well inside STALE_MIDFLIGHT_MS. */
|
|
48
|
+
export const REFRESH_INSTALL_TIMEOUT_MS = 10 * 60_000;
|
|
49
|
+
/** Budget per R2 SNAPSHOT upload. The SDK's createBackup
|
|
50
|
+
* accepts no timeout or AbortSignal, so each call is raced against this
|
|
51
|
+
* (withTimeout): a hung upload fails the cycle into the existing degrade
|
|
52
|
+
* handling with a named error, instead of stranding `refreshing` until the
|
|
53
|
+
* 30-min watchdog. Restores are NOT on this budget any more: a download is
|
|
54
|
+
* judged by the bytes arriving in its target (restoreWithProgress) —
|
|
55
|
+
* a fixed budget abandoned a 481 s restore that then completed.
|
|
56
|
+
* Same class as the other network budgets (observed live transfers run
|
|
57
|
+
* seconds, recorded in `lastRestore.ms`). */
|
|
58
|
+
export const R2_TRANSFER_TIMEOUT_MS = 5 * 60_000;
|
|
59
|
+
/** What the dependency install step runs around the install itself, each
|
|
60
|
+
* bounded: the scratch clone, the seed and its cache swap, the commit (one
|
|
61
|
+
* network budget each) and the harden (the default exec budget). The step's
|
|
62
|
+
* lease is the install budget plus this. */
|
|
63
|
+
export const DEPS_STEP_OVERHEAD_MS = 4 * GIT_NETWORK_TIMEOUT_MS + DEFAULT_EXEC_TIMEOUT_MS;
|
|
64
|
+
|
|
65
|
+
export const errMsg = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
66
|
+
|
|
67
|
+
/** The Worker's one tracer and its `slow` log sink, whose filter drops a
|
|
68
|
+
* refusal's line: the entry's edge and step roots and the refresh instance's
|
|
69
|
+
* `resident.refresh` root all start from these, so one isolate emits one
|
|
70
|
+
* consistent trace (docs/reference/specs/tracing.md item 22). */
|
|
71
|
+
export const tracer = createTracer({ clock: systemClock });
|
|
72
|
+
export const traceSinks = [workerLogSink((line) => console.log(line))];
|
|
73
|
+
|
|
74
|
+
export function registryStub(env: Env) {
|
|
75
|
+
return env.REGISTRY.get(env.REGISTRY.idFromName("registry"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function residentStub(env: Env, resource: string) {
|
|
79
|
+
return getSandbox(env.RESIDENT, resource, { sleepAfter: SLEEP_AFTER });
|
|
80
|
+
}
|