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

Concurrency

magus coordinates parallel work at two distinct scopes, and it helps to keep them apart:

  • Within one run - the scheduler fans a single invocation out across projects and targets, ordered by the dependency graph. This is dependencies and per-target policy (slots, exclusive) doing their job.
  • Across separate runs - the workspace lock stops two independent magus processes from mutating the same project at the same time.

The first is about ordering and fan-out; the second is about mutual exclusion. They solve different problems and neither replaces the other.

Within one run: the scheduler

A single magus run/magus affected invocation builds the dependency graph, then runs targets concurrently where the graph allows. magus\needs edges order the work (dependencies); a target's slots and exclusive policy tune how much of it runs at once (targets). When a daemon is present, that fan-out draws from one shared concurrency pool across every client (daemon).

All of this lives inside one process. It has no bearing on a second magus you start in another terminal - the two invocations have separate graphs and separate schedulers, and neither can see the other.

Across separate runs: the workspace lock

That second invocation is the problem the workspace lock exists for. Two magus processes running at once - two terminals, or two agents - can collide: one running generate or clean rewrites or deletes a project's declared outputs while the other is reading or writing them, and work is lost. Both also write the project's cache. Serializing that is mutual exclusion, which is why needs cannot solve it: needs orders targets inside one run and has no visibility into a separate process. Only a lock does.

So before a non-dry run begins mutating, magus takes a per-project advisory lock for every project the run will touch, holds it for the whole invocation, and releases it at the end. A second magus that wants the same project waits for the first to finish, then proceeds automatically.

Key properties:

  • Per project, not per workspace. Runs on different projects proceed in parallel; only runs on the same project serialize. The lock is not directory- or target-scoped - a project's outputs and cache are the unit being protected, and that is exactly a project.
  • Advisory. It serializes magus processes and nothing else. A raw git clean, an rm, or any other tool ignores it. The guarantee is "no two magus invocations mutate the same project at once," not "the tree is untouchable."
  • Crash-safe. It is an OS file lock (flock), which the kernel releases when the holding process exits or crashes - never a stale PID file that would wedge a project after a Ctrl-C.
  • Taken by every real run, not just generate/clean. Even magus test writes the project's cache and run log, so two concurrent runs on one project are serialized regardless of whether either touches the source tree.

When a run is waiting

If another magus holds the lock, your run does not fail and does not hang silently - it prints one line up front and starts the moment the other finishes:

magus: project web is being changed by another magus process; waiting for it to
finish. This run starts automatically once it does; set MAGUS_NO_WAIT=1 to fail
fast instead.
magus: lock on project web released; starting.

Set MAGUS_NO_WAIT=1 to make a contended run fail fast instead of blocking - useful in CI or a script that would rather error than queue behind another process.

The wait happens at the very start of the invocation, before the concurrency pool is even set up, so a blocked run does not yet appear in magus status (there is nothing running to report - it is queued behind the lock). The stderr line above is how you know why.

Relationship to the daemon

The daemon is the long-lived process that hosts the shared pool and serves clients. When it is coordinating your work, it is the natural single point that knows what is running. The workspace lock is what protects the case the daemon does not cover: two plain magus invocations with no daemon in the loop. The two compose - the lock is the floor that holds even when nothing else is watching.

See also

  • Dependencies: magus\needs and depends_on, how a single run is ordered.
  • Targets: per-target slots and exclusive policy.
  • Daemon: the persistent process and the shared concurrency pool.
  • Cache: what a run writes, and why concurrent writers are serialized.
concurrencyparallelismworkspace-lockschedulerdaemonneedsMAGUS_NO_WAIT
Last updated (38e9171e)
Earlier changes on this page (2)

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.

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.

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.

Pool

The concurrency pool: the shared set of slots that caps how many targets run in parallel on one machine. Its capacity defaults to MAGUS_CONCURRENCY, then 4 on GitHub-hosted runners, then min(NumCPU, 8); magus status and the dashboard report it live. See daemon.

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 daemon.

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 daemon.

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.

Conventions

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