Size a Workspace
Give a Workspace the memory and cpu that its Sandbox needs.
The Size is the most memory and cpu that the whole Sandbox can use. jr2 reserves all of it on a node, so the Sandbox always has it. Write it in the resources option of workspace(). In this example, the Workflow workflows/e2e.ts runs Playwright with 4 GiB of memory and 2 cpus:
import { z } from "zod";
import { command, fail, jr2Setup, workspace, type Workspaced } from "@jr2/orchestrator";
const runInput = z.object({ branch: z.string() });
type BodyInput = Workspaced<z.infer<typeof runInput>, "app">;
const body = jr2Setup({
types: {} as { input: BodyInput },
actors: {
// JR2_CPUS holds the cpu limit of the container. Playwright does not read that limit.
e2e: command(["sh", "-c", "npx playwright test --workers=$JR2_CPUS"], { timeout: "30m" }),
fail: fail(),
},
}).createMachine({
id: "e2e",
on: { "workspace.lost": ".lost" },
initial: "testing",
states: {
testing: {
invoke: {
src: "e2e",
onDone: [
{
guard: ({ event }) => event.output.outcome === "exited" && event.output.code === 0,
target: "passed",
},
{ target: "failed" },
],
},
},
passed: { type: "final" },
// Not final states: each one fails the run, and the run ends `failed`.
failed: { invoke: { src: "fail", input: { reason: "the Playwright tests did not pass" } } },
lost: { invoke: { src: "fail", input: { reason: "the Sandbox was lost" } } },
},
});
export const machine = workspace(body, {
input: runInput,
// The Size of the whole Sandbox: 4 GiB of memory and 2 cpus. jr2 reserves all of it on a node.
resources: { limits: { memory: "4Gi", cpu: "2" } },
repos: { app: "https://github.com/you/app.git" },
spec: ({ input }) => ({ branch: input.branch }),
});The Sandbox Image must have Playwright and its browsers. Add tools to the Sandbox tells how.
Choose the numbers
A Size is { limits: { memory, cpu } }, and nothing else. Each value is a Kubernetes quantity: "4Gi" or "512Mi" for memory, "2" or "1500m" for cpu.
The kit default is memory 2Gi and cpu 1. It is enough for tsc and vitest, for cargo build, and for Playwright with 2 workers. State a larger Size for Playwright with more workers, or for a large monorepo.
A larger Size is not free. The Sandbox holds all of its Size while the run waits, also at a Gate. When no node has room for the Size, the Workspace waits in the Placing state, with no time limit. Workspaces tells how to see the wait.
jr2 up warns when no node can hold a Size now. It does not stop, because the nodes can change. For example, a kind node with 16 cpus cannot hold a Size of memory 64Gi and cpu 32:
…
sandbox nodes: jr2-control-plane
warning: no Sandbox node right now has the allocatable for a Size of memory 64Gi, cpu 32 (workflow "e2e") — its Sandbox stays Pending until one does
…Where the Size comes from
The Size is a fact of the Machine. A run cannot change it. jr2 uses the first of these that states a Size:
customize(machine, { resources }), in the Instance that registers the Machine.resourceson theworkspace().sandbox.resourcesinjr2.config.ts, such assandbox: { resources: { limits: { memory: "3Gi" } } }.- The kit default.
These rules apply to the order:
customize()writes over the Size of theworkspace()one field at a time. Thuscustomize(machine, { resources: { limits: { memory: "6Gi" } } })keeps the cpu of the Machine.sandbox.resourcesapplies only to a Workspace that states no Size. It never changes a Size that a Machine states.- A field that the chosen Size does not state comes from the kit default. For example, under
resources: { limits: { memory: "4Gi" } }, the cpu is1, also whensandbox.resourcesstates a cpu.
Use customize() to change the Size of a Machine from a package, such as task from @jr2/machines. See Bind a shipped Machine's Open parts.
How jr2 divides the Size in the pod
The pod has the Size as one limit. Inside the pod, jr2 divides it among the containers:
| Container | Its part |
|---|---|
| The Custodian | A fixed part: memory 64Mi, cpu 50m. |
| The User Container | The part that user.resources gives. With no part, it shares the rest. |
| The Sandbox Effector | No part of its own. It shares the rest. |
The harness container | The rest, as its own limit. The Agent's processes and the commands run here. |
jr2 up refuses a Size that leaves the harness container less than 256Mi of memory, or no cpu.
To give the User Container a part, write user as an object: user: { image: import.meta.resolve("./shell"), resources: { limits: { memory: "256Mi", cpu: "250m" } } }. The part comes out of the part of the harness container. It is not added to the Size.
The Size holds only memory and cpu. jr2 also sets these, and you cannot change them:
/dev/shmin theharnesscontainer is 25% of the container's memory, and not less than64Mi. It counts in that memory. To get more, state a larger Size./workhas a disk request of10Gi, with no limit. The disk is not part of the Size.
Use the cpus
jr2 enforces the cpu limit. Many tools read the limit, such as Node's availableParallelism(), Go, cargo and the JVM. For tools that do not read it, these environment variables in the harness container hold its cpu limit, rounded up to a whole number: JR2_CPUS, OMP_NUM_THREADS, PYTHON_CPU_COUNT and GOMAXPROCS.
Node's os.cpus() gives the cpus of the node, not the limit. Playwright uses it to choose its number of workers. Thus the example gives --workers=$JR2_CPUS. A command sees these variables.
When the memory is not enough
The Harness watches the memory of its container. Near the limit, it stops all of the Agent's processes. Then the Agent's bash call returns this text, and the Agent can try again with less:
killed: memory limit (peak <peak> of <limit>); use fewer workers or a larger SizeThe Harness also watches when no bash call runs. A process that a Turn left running, such as a dev server, can pass the limit while the workflow waits at a Gate. Then no bash call returns the text. jr2 tells the next Agent that works in the Workspace: the next Turn, or an open Turn that jr2 asks again.
The Harness can miss a fast increase. Then the kernel stops the whole container, and the Turn faults. The reason starts with memory limit:
memory limit (OOMKilled, limit <memory>): the Workspace's processes passed the Harness container's memory limit and the kernel killed the container, the conversation with it. Use fewer workers, or give the Workspace a larger Size (ADR-0060, ADR-0061).The next Agent in the Workspace gets a notice of the kill. When you see either text again and again, state a larger Size.
Rules
workspace(),customize()and the config refuse a key other thanlimits, where you write it.<where>isworkspace(): resources,customize(): resources,<file> at sandbox.resources,workspace() user.resourcesorcustomize() user.resources:<where>.requests is not accepted — jr2 reserves the whole Size, so every request equals its limit — a Size is { limits: { memory?, cpu? } } and nothing else — Agent Substrate's template size (ADR-0060)and<where>.<key> is not accepted — a Size is { limits: { memory?, cpu? } } and nothing else — Agent Substrate's template size (ADR-0060).- They refuse a key in
limitsother thanmemoryandcpu:<where>.limits.<key> is not accepted — a Size is { limits: { memory?, cpu? } } and nothing else — Agent Substrate's template size (ADR-0060). - They refuse a value that is not a quantity more than zero:
<where>.limits.memory: <value> is not a positive quantity (e.g. "2Gi", "512Mi")and<where>.limits.cpu: <value> is not a positive quantity (e.g. "2", "1500m"). - They refuse a Size, or a
limits, that is not an object:<where>: expected an object (got <value>) — a Size is { limits: { memory?, cpu? } } and nothing else — Agent Substrate's template size (ADR-0060)and the same text for<where>.limits. customize()refusesresourcesfor a Machine that has noworkspace():customize(): `resources` say what a SANDBOX is made of, how big it is and which Repos it attaches, and machine "<id>" composes none — only a workspace() wrapper carries those seats, that Size and those slots (ADR-0049, ADR-0037, ADR-0051, ADR-0060).jr2 uprefuses a Size that leaves theharnesscontainer less than256Miof memory. The text names each number and where it came from. The part− the User Container's <memory> (its user.resources.limits.memory)is there only whenuser.resourcesgives a memory:refusing: workflow "<workflow>": the Size leaves the Harness container <memory> memory, below its 256Mi floor: memory <size> (<source>) − the Custodian's 64Mi − the User Container's <memory> (its user.resources.limits.memory) — raise the Size's limits.memory, or shrink the User Container's split (ADR-0060). The text can also name the Machine that states the Size, and it can end with thecustomize()line that changes the Size. For cpu, the text holdsthe Size leaves the Harness container no cpu:.jr2 uprefuses a cluster that drops pod-level resources:refusing: <context> (Kubernetes <version>) drops pod-level resources — the PodLevelResources feature gate is off, and a Workspace's Size cannot hold (ADR-0060). Enable the gate on the API server and kubelets. The namespace was created; nothing else was applied.
Read next
- Workspaces: the Size, and Placing.
- Add tools to the Sandbox: the User Container.
- Reference:
Size,UserContainer,sandbox.resources,jr2 up - ADR-0060: a Workspace states its Size, and jr2 reserves all of it.
- ADR-0061: a memory kill ends the Agent's processes, not the Harness.
- ADR-0064: a Workspace waits for capacity.