magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
6 min read

MGS3009: machine budget exhausted

A step was not started. What it needs does not fit in this machine's build budget alongside the claims other live magus invocations are already holding.

magus never queues behind another magus invocation, so a budget they fill is refused immediately, naming who holds it:

[MGS3009] not starting (root) ci: this machine's build budget is full; it runs test,
  which declares 10.0 GiB and takes 6 slots, and 2.0 GiB of 48.0 GiB is left and 2 of
  8 slots are free; held by pid 41221 (root) test (10.0 GiB), in
  /Users/x/repos/magus-worktrees/polish.

A step kept out only by claims of its own process or its own run (the siblings of one fan-out) waits for them instead, and never sees this code.

The code fires in three cases, none of which a retry-without-delay fixes.

The machine is full right now. The step above; the same command succeeds once the holder finishes.

The declaration cannot fit at all. No position in any queue seats a step whose declaration exceeds the whole budget, so magus says so immediately rather than waiting forever:

[MGS3009] refusing to start (root) ci: it runs test, which declares 64.0 GiB, and takes
  8 slots, which does not fit in this machine's whole build budget of 48.0 GiB across
  8 slots. Waiting would not help; that budget is 75% of the memory available here; the
  rest runs the OS and everything else. Run `magus doctor` and read MGS1030, which
  compares this declaration to the peak memory magus measured: correct the declaration
  if it has drifted; if it is honest, get a bigger machine.

A nested magus cannot tell who its parent is. magus passes MAGUS_INVOCATION_ANCESTORS to every magus it starts, and admission uses it to excuse a run from the claim its own parent is holding. A magusfile that clears the environment breaks that, and the affected run would be refused by a budget its own parent filled - a permanent deadlock, since the parent cannot finish until this run does:

[MGS3009] not starting docs ci: this magus runs underneath another one but was started
  without MAGUS_INVOCATION_ANCESTORS, so it cannot tell its own parent's claim from a
  stranger's and will not queue behind a run that is waiting for it; ... Pass that
  variable through to nested magus invocations, or let magus set it by not clearing
  the environment.

None of them exits 1, and they do not all exit the same way. A busy machine exits 75, EX_TEMPFAIL from sysexits.h, meaning "failed, try again later": the machine is full now and the same command runs once it drains. The other two exit 78, EX_CONFIG, because no amount of retrying changes their answer. A declaration larger than the whole budget and a nested magus with no ancestry both refuse identically on every retry, so a wrapper that retries on 75 stops on those instead of looping. A run where real targets also failed exits 1 as usual: a broken build does not become a scheduling problem because a peer happened to be busy too.

Why

The concurrency limiter is per-process. Every magus invocation admits work up to its own capacity, and nothing sums those capacities, so N invocations across N worktrees admit N budgets' worth of work against one machine. That is not a theoretical concern on a developer machine running several agents at once: it is how a workstation ends up tens of gigabytes deep in swap with every individual run comfortably inside its own budget.

memory_mb is the declaration that makes this arbitrable, and the broker is what arbitrates it. There is one broker per user, so there is one budget per machine: every magus asks the same process, and the answer accounts for the worktrees this run knows nothing about. A run starts the broker if none is up (and it exits by itself ten minutes after it last held anything), because admission has no other arbiter; see the broker and the server.

It refuses rather than queuing behind another invocation. magus never waits on another magus invocation, so a step that does not fit right now is told so immediately, naming who holds the budget, rather than parked until room frees. What it does wait for is its own run: a step kept out by claims its own process or its own root invocation holds waits for them, since refusing would fail a run over work it is already doing.

The budget is derived from the memory the broker may commit, not from a reading of what is free right now. That keeps the verdict deterministic: the same command on the same machine reaches the same decision regardless of what a browser is doing, and the message names figures a reader can check afterwards. Observed pressure has a separate, advisory job - the memory-headroom warning a run emits while it works.

Targets that declare nothing claim no memory and are never held for that reason. An absent memory_mb means magus does not know what the target needs, and inventing a figure would gate work on a guess. Every step still claims its concurrency slots, which is the half every step spends.

A composed target inherits its chain's largest declaration. Only the target you name is scheduled as a step. So ci, which composes test through ctx.needs, would otherwise run the heaviest target in the workspace as if it had declared nothing, and magus affected ci is exactly the invocation that fills a machine. The maximum rather than the sum, because a chain runs its steps in order.

A declaration is a ceiling, and measured runs size the claim under it. A step claims 1.25 times the highest peak magus recorded across successful runs with the same charms and forwarded args, or across every run of the target when that shape has fewer than three, and never more than the declaration. With fewer than three successful measured runs of any shape it claims the declaration. So a -- -run TestOne run claims what narrowed runs reach rather than what the whole suite does, and a target declaring nothing still claims only its slots.

A run that loses the broker finishes, unless you said otherwise. Under the default broker: best-effort, a broker that dies mid-run takes its claims with it, the run says once that it is no longer arbitrated, and it completes; its running steps re-assert their claims on the next broker to come up. Under broker: required a step that finds no broker is refused instead (MGS3022). Every claim rides its run's connection to the broker, so a run that dies, even to SIGKILL, releases its claims at once and nothing has to release cleanly.

Resolution

  1. Retry once a peer finishes. The message names the pid, project, target and directory holding each claim, and magus status shows the whole budget across worktrees. Exit 75 is the signal a script or an agent should branch on to retry rather than investigate.

  2. Check that the declaration is honest. A memory_mb well above what the target actually peaks at holds up work that would have fit. magus doctor reports a declaration that disagrees with the peak resident memory magus has measured across recent runs (MGS1030), and that figure is the one to declare.

  3. Reduce what the target needs. A suite that declares ten gigabytes because every concurrent test binary carries the race detector's shadow memory can often be sharded, or run with lower in-tool parallelism, and then declare less.

  4. Run on a bigger machine. The budget is a fraction of what the host has, so a target that cannot fit on an idle machine is a target this machine cannot run.

See also

  • Concurrency: the three scopes magus coordinates, and where this one sits.
  • The broker and the server: the process that owns the budget, and why a run starts one.
  • MGS3022: the refusal when no broker answers under broker: required.
  • memory_mb: declaring what a target needs, and how magus converts it into concurrency slots within one process.
  • MGS1030: the declaration that has drifted from what the target measurably uses.
  • MGS3007: the other code about a run the process context makes impossible.
MGS3009memorymemory_mbconcurrencyworktreesadmissionbroker
Last updated (16f4370e)
Earlier changes on this page (5)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. See charms.

Ward

A coded diagnostic that inspects a resolved op and nudges or blocks an anti-pattern before it runs. See wards.

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. See affected.

Broker

The per-user background process that holds this host's capacity: the machine budget every run claims slots from, and the shared services runs keep warm. A run starts it on demand; broker: off in magus.yaml runs without one. See server.

Server

The background process a person starts with magus server start. It serves MCP, the console, background jobs and the warm knowledge graph, and adopts nested magus calls into one pool. See server.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Slot

One unit of the pool's capacity. A target acquires the slots it needs to run (most take one) and releases them when it finishes; the pool tracks capacity (total slots), running (acquired), and queued (blocked). See server.

Concurrency

How many targets run at once. It is bounded by the pool's capacity and set with --concurrency, MAGUS_CONCURRENCY, or the concurrency config key. See server.

Invocation

One magus process's recorded facts - the targets it finished, their outcomes, the lease it acted as, and the session it ran in when a host delivered one - kept in a repo-scoped store every worktree shares. magus session lists them; the store prunes itself by last-fact age.

Job

The unit of delegated work, and one row of the job store: what an orchestrating agent handed out, with its goal, the checkpoint it was cut against, the paths it may write or must not touch, and the one check it runs. A job's holder is either a session, for work an orchestrator handed out, or the server, for its own maintenance. The store records; the agent guard is what reads those facts back when grading a write. See doctrine.

A job is not a run. magus run build web is a run, and no job exists for it. A job causes runs: its check executes as one, and a server job records the invocation of its last one. Jobs are listed with magus ls jobs and in the console's Jobs view; runs are listed in the Runs view.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate.

Lease

The grant a holder takes on a job: the write and read paths that job declared, enforced in the checkout that took it with magus job exec. A job is the piece of work; a lease is permission over it.

Advisor

One read-only check from the advice suite: it reads the changeset through magus and writes one titled section of findings. The same advisors run as a pull request comment in CI and inside magus diff --impact locally.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.