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

MGS3007: project lock held by an ancestor run

A magus run tried to lock a project that one of its own ancestor invocations already holds. The ancestor cannot release it, because the ancestor is blocked waiting for this run to exit, so the run is refused instead of queued.

[MGS3007] project . is locked by the magus run this one is nested inside
  (pid 42636 (magus run screenshots .)), which cannot finish until this one does.
  Waiting would never end, so it is refused instead.

Why

A run takes the EXCLUSIVE workspace lock for every project it selects and holds it for the whole invocation. That lock exists to stop two magus invocations from mutating one project at once, and for two unrelated runs, waiting is exactly right: the holder finishes, the lock frees, the waiter proceeds. The wait is announced up front and reprinted every 15 seconds.

Nesting breaks that reasoning. When the holder is an ancestor of the waiter, the holder is inside os.exec/magus.run, waiting for this very process to exit. Neither side can move. flock cannot tell this apart from ordinary contention, and neither can a timeout - a timeout only converts a hang into a late failure that still does not say why. magus carries each invocation's ancestry (MAGUS_INVOCATION_ANCESTORS, inherited like MAGUS_LEVEL, and recorded in the lock's owner sidecar) so the two cases can be told apart at the moment of acquisition.

The usual shape is a target that shells out to magus against its own project:

export fun deploy(ctx: magus\Context, args: [str]) > void {
    magus\run(["release"]);   // no project selector: the project this run holds
}

Every spawn that goes through magus's own subprocess layer is covered: os.exec("magus", ...), magus.cmd("run", ...), magus.run(...), and a shell script several levels down all inherit the ancestry and all raise this. A magus started by something that does NOT inherit that environment - a long-running service process, a detached daemon - is not covered, and still waits.

Resolution

  1. Express the dependency instead of spawning one. A target that needs another target's work wants an edge, not a subprocess:

    export fun deploy(ctx: magus\Context, args: [str]) > void {
        ctx.needs(release);
    }
    

    One invocation then runs both, under one lock, with the cache and the affected-set tracking intact - none of which a nested process inherits.

  2. Target a project the outer run does not hold. Nesting itself is fine when the project sets are disjoint: a root target may run magus\run(["build", "console"]) while holding only the root lock. Name the project explicitly so the selection cannot widen to the one the outer run holds - a bare magus\run(["build"]) selects by directory and can pick up the caller's own project.

  3. Reach for a read-only subcommand. magus ls, describe, graph, and query take no project lock, so a target may call them freely. The typed in-process members (magus\projects, magus\targets, magus\affected, magus\graph) are better still: they answer from the workspace already open and never fork.

  4. If the work genuinely belongs in a script, keep it in the script. A sequence of magus commands driven by a shell script is a valid thing to have; it just cannot be wrapped in a target that the commands then contend with. Run the script directly.

See also

  • ctx.needs: the dependency edge to use instead of a nested run.
  • magus status: names the lock's holder and every waiter, including this case.
  • MGS5004: the other code about a run that cannot proceed in the process context it was given.
MGS3007locksnested rundeadlockproject lockmagus.run
Last updated (39ac696e)
Earlier changes on this page (1)

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.

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.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

Affected

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

Service

A long-running or shared process magus manages across runs, distinct from a one-shot target. See services.

Daemon

The background magus host that owns shared state such as services and the warm knowledge graph. See daemon.

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.

Queued

A target that wants a slot while the pool is full; it blocks first-in-first-out until a slot frees. The dashboard colors a sample with queued > 0 accordingly. See daemon.

Lease

One row of the lease ledger: a piece of work an orchestrating agent handed out, with its goal, the checkpoint it was cut against, and the paths it owns or must not touch. The ledger records; the agent guard is what reads those facts back when grading a write. See doctrine.

Conventions

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