Skip to content

harn admission

harn admission run --resource <name> [options] -- <command...>
harn admission run --detach --resource <name> -- <command...>
harn admission wait <job-dir> [options]
harn admission jobs [options]
harn admission status [options]

Machine-wide admission control for heavy jobs: a crash-safe FIFO slot queue with no daemon. Two production builds, two browser-QA matrices, or a build and a matrix started at the same moment slow each other down more than they would running back to back. admission run wraps any command so that at most a fixed number of holders per named resource run at once on one machine; everyone else waits their turn.

harn qa-run uses this protocol internally (resource browser-qa, capacity 2 by default). The admission command is the same mechanism as a generic wrapper for everything else. Embedding hosts that need to join those queues from their own commands import acquireAdmission and admissionBaseDir from harnery/lib/admission so they share the on-disk tickets instead of wrapping the CLI.

Terminal window
harn admission run --resource build -- npm run build

Joins the FIFO queue for the named resource, waits for a slot, runs the command with inherited stdio, and releases the slot when the command exits. The wrapper exits with the wrapped command’s own exit code, so it composes into scripts transparently. Everything after -- reaches the child untouched, as an argument vector, never re-parsed by a shell: the first token is the executable.

The resource name is just a string agreed on by the participants: every wrapper naming --resource build takes turns with every other wrapper naming build, and has nothing to do with wrappers naming e2e.

Flag Description
--resource <name> Required. Admission resource to queue on, e.g. build.
--capacity <n> Concurrent holders this machine should allow (1 to 32; default 1). Advisory: the smallest capacity among concurrent waiters effectively governs.
--timeout <minutes> Maximum admission wait before giving up (1 to 1440; default 60).
--label <text> Holder description shown in status listings. Default: the command itself.
--detach Run the command as a durable job instead of in the foreground. See below.
--json With --detach, print the job envelope as JSON.

Exit codes: the child’s exit code on a completed run; 4 when the admission wait timed out (the command never ran); 1 on a usage error, a spawn failure, or a child killed by a signal.

Terminal window
harn admission run --detach --resource build -- npm run build

A foreground wrapper dies with its terminal. Close the tab, drop the SSH connection, or lose the bridge between an agent and the machine it drives, and the build dies halfway through, usually with no record of how far it got. --detach removes the client from the critical path:

  1. The client mints a job record, launches a detached supervisor process, prints the job id and directory, and exits 0. It never waits for the slot.
  2. The supervisor queues for the admission slot, runs the command with its output on disk, refreshes a heartbeat every 15 seconds, and writes the terminal record with the command’s exit code.
  3. Any later client reads that record. Reconnecting is a read, so it can happen from a different terminal, a different session, or after an hour of disconnection.
[info] detached job 544726e2-5ef2-42f4-afd6-9365ba1e4c41 (supervisor pid 2587475)
[info] job dir: /tmp/harnery-admission-jobs/jobs/544726e2-5ef2-42f4-afd6-9365ba1e4c41
[info] log: /tmp/harnery-admission-jobs/jobs/544726e2-5ef2-42f4-afd6-9365ba1e4c41/job.log
[info] reconnect: harn admission wait /tmp/harnery-admission-jobs/jobs/544726e2-5ef2-42f4-afd6-9365ba1e4c41

Each job directory holds three files:

File Contents
job.json The immutable description: job id, resource, capacity, label, argument vector, working directory, creation time.
status.json The mutable state, rewritten atomically: launching, queued, running, or completed, plus the supervisor PID, the heartbeat, and the final exit_code and signal.
job.log The command’s merged stdout and stderr.

Job records live beside the queue state, under $TMPDIR/harnery-admission-jobs by default, or the directory named by HARNERY_JOBS_DIR.

Exit codes with --detach: 0 once the job is launched, 1 on a usage error or a failure to launch the supervisor. The job’s own outcome comes from admission wait.

Terminal window
harn admission wait /tmp/harnery-admission-jobs/jobs/544726e2-...

Polls the job record every 2 seconds until it settles, printing a line on each state transition, then exits with the job’s own exit code. Reconnecting a second time to a job that already finished gives the same answer, because the record is what decides, not the client’s memory of it.

--timeout <minutes> bounds the wait (1 to 1440, default 120). --json prints the full job report.

Exit codes: the job’s own exit code once it completes; 1 on a usage error or an unreadable job record; 4 when the job is dead; 5 when the wait timed out while the job was still running.

Terminal window
harn admission jobs

Lists recent jobs, newest first, with id, state, resource, exit code, heartbeat age, and label. --limit <n> bounds the listing (default 20) and --json emits the full reports.

[info] job 544726e2-...: completed, resource smoke-test, exit 0, heartbeat 33s ago, age 39s - npm run build
[warn] job e42e838b-...: dead, resource smoke-test, pid 2687437 not running, heartbeat 2s ago, age 2s - npm run build
Terminal window
harn admission status

Lists each resource present in the admission state directory with its current slot holders and FIFO waiters: label, PID, and since when each has been holding or waiting. Dead entries are pruned as a side effect of being listed. Detached jobs still in flight are listed underneath. --resource <name> narrows to one resource; --json emits a machine-readable snapshot.

Classification is fail-closed, the same rule harn qa-status applies to a detached QA run. Liveness is proven by the supervisor’s PID, never assumed from the status document:

  • launching: the record exists and the supervisor is starting.
  • queued: the supervisor is waiting for a slot on its resource.
  • running: the command is executing. A heartbeat older than 120 seconds earns a staleness warning but stays running while the PID is alive.
  • completed: terminal. exit_code and signal are final.
  • dead: derived, never written. The state is non-terminal, the supervisor PID is gone, and no terminal record was written. Something killed the supervisor.

Never relaunch a job because a client lost sight of it. A disconnect says nothing about the job, and the whole point of the record is that it outlives the connection. Ask admission wait or admission jobs first: relaunch only after one of them reports dead, and read job.log before you do, because the wrapped command may have finished its side effects before the supervisor died. Note that killing a supervisor does not kill the command it launched, so a dead record can coexist with a command still running.

The queue is plain files on one machine, designed so that no crash can wedge it:

  • FIFO tickets. Joining the queue writes a ticket file whose name starts with a millisecond timestamp; lexical filename order is enqueue order. Admission is an atomic rename from the tickets/ directory to held/.
  • Self-correcting races. When two waiters admit themselves against the same freed slot, whoever sorts later re-lists the held directory, sees the over-admission, renames itself back, and rejoins the queue with its original position (the filename keeps its enqueue timestamp).
  • Dead-PID and TTL pruning. Every participant prunes entries whose PID is no longer alive or whose age exceeds the TTL (default 6 hours) whenever it looks at the queue. A crashed holder’s slot frees as soon as anyone else looks; no daemon, no manual cleanup.
  • Advisory, single-machine scope. Only processes that opt in by wrapping themselves participate, and the state is local files, so the protocol coordinates one machine only. It is a cooperation mechanism, not a lock a hostile process cannot bypass.
  • State location. Queue state lives under $TMPDIR/harnery-admission by default, or the directory named by the HARNERY_ADMISSION_DIR environment variable. Because it sits in the temp directory, it is cleared on reboot, which is correct: stale queue state must not outlive the boot.
Terminal window
# Make production builds take turns on this machine
harn admission run --resource build -- npm run build
# Let two documentation renders overlap, but never a third
harn admission run --resource docs-render --capacity 2 -- npm run docs:build
# Who is holding what right now?
harn admission status
# Start a build that survives losing this terminal, then reconnect to it
harn admission run --detach --resource build -- npm run build
harn admission jobs
harn admission wait /tmp/harnery-admission-jobs/jobs/<job-id>
  • harn qa-run: queues on the browser-qa resource by default, records the wait as wall_time_ms.queue, and turns an admission timeout into an incomplete result with an admission blocker.
  • harn qa-status: --queue renders the browser-qa view of this same state.