@runblackcat/sdk 0.1.0-alpha.70 → 0.1.0-alpha.72
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 +41 -0
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +7 -1
- package/sdk/dist/index.js +5 -2
package/README.md
CHANGED
|
@@ -212,6 +212,47 @@ blackcat_placement_rejected_total{reason="…"} no_capacity
|
|
|
212
212
|
A fleet pinned at its ceiling shows up as `blackcat_capacity_target_nodes`
|
|
213
213
|
persistently above `blackcat_nodes`.
|
|
214
214
|
|
|
215
|
+
## Recommended production shape: bare metal
|
|
216
|
+
|
|
217
|
+
Run workers on bare metal, one or a few large `.metal` hosts, and keep nested virtualization (a VM with `/dev/kvm`) as the quick-start
|
|
218
|
+
default: it launches in under a minute and needs no special quota. On metal every guest page fault costs about 7.6 times less than
|
|
219
|
+
nested, so the same fleet restores 2 to 3 times faster, fans out 30 times faster at 100 clones, costs a third as much per clone and
|
|
220
|
+
does not lose VMs to nested VM-entry failures.
|
|
221
|
+
|
|
222
|
+
| | bare metal (`c7i.metal-24xl`) | nested (`m8i.4xlarge`) |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| create p50 | 29 ms | 60 ms |
|
|
225
|
+
| first exec | 33 ms | 67 ms |
|
|
226
|
+
| snapshot restore | 13 ms | 43 ms |
|
|
227
|
+
| fan-out of 100 clones | 0.6 s | 17.9 s |
|
|
228
|
+
| cost per clone-hour | $0.0043 | $0.0132 |
|
|
229
|
+
| VM-entry deaths | 0 | 2.9 per 1,000 hot restores, 325 per 1,000 at density |
|
|
230
|
+
|
|
231
|
+
(Bake-off, 2 vCPU / 1 GiB app template, `contrib/bench/bakeoff/RESULTS.md`, "Huge pages". The hosts are different CPU
|
|
232
|
+
generations and metal has 12 times the CPUs, so the fan-out column is not virtualization alone. Metal is cheaper per clone because it
|
|
233
|
+
carries about 1,000 clones where the nested host was measured at 64.)
|
|
234
|
+
|
|
235
|
+
North star on one `c7i.metal-48xl` (500 clones of a built-from-source web app at once, each running a short test then destroyed):
|
|
236
|
+
99% done 112 s after the PR arrived, all 500 passed with no replacements and no VMM deaths, about $0.34 per PR from a stopped node,
|
|
237
|
+
against 300 s, 264 replacements and about $1.00 on five `m8i.8xlarge`.
|
|
238
|
+
|
|
239
|
+
Trade-offs, plainly:
|
|
240
|
+
|
|
241
|
+
* **Boot.** A metal reboot has been quoted at about 7.5 minutes. Measured on a `c7i.metal-48xl`: a fresh launch was registered with SSM
|
|
242
|
+
in 74 s and a stopped node was serving again 47 s after `StartInstances`, but stopping it took 320 s (unbilled). Time from a
|
|
243
|
+
planner launch to an enrolled node on metal has not been measured (see
|
|
244
|
+
`contrib/bench/results/2026-10-04-wave-c-metal-northstar.md`). Nested hosts register in 40 to 45 s.
|
|
245
|
+
* **Coarse sizes.** Metal is `.metal` sizes only: 96 or 192 vCPUs on the `c7i` family. A small or spiky fleet pays for a whole box
|
|
246
|
+
or runs nested.
|
|
247
|
+
* **Thin Spot capacity.** Metal Spot pools are small; the planner falls back to on-demand when one refuses, and the price you plan
|
|
248
|
+
with is the cheaper Spot one.
|
|
249
|
+
* **Large blast radius.** One host carries up to about a thousand clones. Losing it, or restarting its agent, hits all of them; keep
|
|
250
|
+
`fleet max-clones-per-host` as the cap you can accept and keep more than one host for anything that must survive a host loss.
|
|
251
|
+
|
|
252
|
+
The capacity planner prefers metal when a reservation's `needed_by` allows its boot time and the price per clone is lower
|
|
253
|
+
([docs/capacity-planning.md](docs/capacity-planning.md)); `blackcat_vm_entry_deaths` counts the VM-entry failures that make nested
|
|
254
|
+
hosts unreliable at density. Setup: [docs/bare-mode.md](docs/bare-mode.md#recommended-production-shape-bare-metal).
|
|
255
|
+
|
|
215
256
|
## Install (alpha)
|
|
216
257
|
|
|
217
258
|
Identical on bare metal and on AWS:
|
package/package.json
CHANGED
package/sdk/dist/index.d.ts
CHANGED
|
@@ -262,7 +262,13 @@ export interface CreateTemplateResponse {
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
export type CreateSandboxInput = {
|
|
265
|
-
|
|
265
|
+
/**
|
|
266
|
+
* The template name or version id. Omitted, it is the tenant's `default`
|
|
267
|
+
* template, which the gateway builds on the tenant's first use: that first
|
|
268
|
+
* create fails with a 503 `default_template_building` until the build is
|
|
269
|
+
* ready, and repeating the same request is how to wait for it.
|
|
270
|
+
*/
|
|
271
|
+
template?: string;
|
|
266
272
|
/** Must equal the checkpoint's vCPUs, which are fixed at capture; omit it. */
|
|
267
273
|
vcpus?: number;
|
|
268
274
|
/**
|
package/sdk/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ var BLACKCAT_ERROR_CODES = {
|
|
|
18
18
|
SANDBOX_TRANSITIONING: "sandbox_transitioning",
|
|
19
19
|
FLEET_UPGRADE_QUIESCING: "fleet_upgrade_quiescing"
|
|
20
20
|
};
|
|
21
|
+
function withTemplate(input) {
|
|
22
|
+
return { ...input, template: input.template ?? "default" };
|
|
23
|
+
}
|
|
21
24
|
|
|
22
25
|
class BlackcatApiError extends Error {
|
|
23
26
|
status;
|
|
@@ -444,10 +447,10 @@ class SandboxesResource {
|
|
|
444
447
|
return this.client.json("GET", "/v1/sandboxes", undefined, "token");
|
|
445
448
|
}
|
|
446
449
|
async create(input) {
|
|
447
|
-
return parseCreateSandboxResponse(await this.client.json("POST", "/v1/sandboxes", input, "token", false), input.request_id);
|
|
450
|
+
return parseCreateSandboxResponse(await this.client.json("POST", "/v1/sandboxes", withTemplate(input), "token", false), input.request_id);
|
|
448
451
|
}
|
|
449
452
|
async createBatch(input) {
|
|
450
|
-
return parseSandboxBatch(await this.client.json("POST", "/v1/sandboxes", input, "token", false), input.request_id);
|
|
453
|
+
return parseSandboxBatch(await this.client.json("POST", "/v1/sandboxes", withTemplate(input), "token", false), input.request_id);
|
|
451
454
|
}
|
|
452
455
|
async batch(requestId) {
|
|
453
456
|
return parseSandboxBatch(await this.client.json("GET", `/v1/sandboxes/batches/${pathSegment(requestId)}`, undefined, "token"), requestId);
|