Appendix A - 10. Build-serializer (M=8 semaphore)

The Structure of Build-serializer (M=8 semaphore) — its shape at a glance:

Each of the routed tools acquires one of M slots before running and releases after; the semaphore lets concurrency rise to the host's capacity and caps it there.

flowchart LR
  T1[Compiler] --> Sem{{Semaphore, M=8}}
  T2[Type-checker] --> Sem
  T3[Query tool] --> Sem
  Sem -->|slot free| Run([Tool runs])
  Sem -->|8 in flight| Wait([Queue for a slot])

Accessible description: several heavy build tools each acquire one of eight semaphore slots before running; a tool runs when a slot is free and queues when all eight are in flight, so concurrency rises to the host's capacity and is capped there.

Projected from the catalogue entry mediators-and-resource-locks / Build-serializer (M=8 semaphore).

On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns

Intent

Intent — A host-level counting semaphore (M=8) over the adjacent heavy-compute tools (dotnet build, tsc, csharp-query, jedi lints, pyright), so concurrent worktrees get parallelism up to the machine's capacity without oversubscribing it.

Motivation

The heavy build tools are the same shared-host hazard as dotnet test: N worktrees compiling, type-checking, and running Roslyn queries at once saturate CPU and memory and slow everyone. But unlike dotnet test, these tools are numerous and mostly parallel-safe, so the failure to avoid is both oversubscription (too many at once melts the host) and over-serialization (N=1 would waste cores and stall the fleet).

Applicability

Structure

The Structure diagram appears at the top of this page.

Sample Code

The lock's cardinality follows the resource's contention profile: a bounded semaphore for parallel-safe but heavy work. Each routed tool acquires a slot before running; an enforcer refuses the un-mediated call so the cap can't be skipped.

import fcntl

M = 8   # tuned to the host's core / memory budget — parallel-safe but heavy work

def with_build_slot(lock_path: str, run_tool):
    with open(lock_path, "w") as lock:
        for slot in range(M):                          # byte-range semaphore: M independent 1-byte locks
            try:
                fcntl.lockf(lock, fcntl.LOCK_EX | fcntl.LOCK_NB, 1, slot)
                return run_tool()                      # hold the slot for the tool's duration
            except BlockingIOError:
                continue
        raise SystemExit("all build slots busy — queue and retry")

def enforce_mediated(argv0: str, cwd: str) -> None:
    if is_agent_worktree(cwd):                         # the raw call is banned from an agent worktree
        raise SystemExit(f"run {argv0} through the build mediator, not directly")

Consequences

Example use within DocAble

Contents
© James C. Davis, 2026–present