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.
admission run
Section titled “admission run”harn admission run --resource build -- npm run buildJoins 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.
Durable jobs: run –detach
Section titled “Durable jobs: run –detach”harn admission run --detach --resource build -- npm run buildA 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:
- 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.
- 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.
- 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-9365ba1e4c41Each 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.
admission wait
Section titled “admission wait”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.
admission jobs
Section titled “admission jobs”harn admission jobsLists 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 buildadmission status
Section titled “admission status”harn admission statusLists 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.
Reading a job record
Section titled “Reading a job record”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 staysrunningwhile the PID is alive.completed: terminal.exit_codeandsignalare 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 protocol
Section titled “The protocol”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 toheld/. - 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-admissionby default, or the directory named by theHARNERY_ADMISSION_DIRenvironment variable. Because it sits in the temp directory, it is cleared on reboot, which is correct: stale queue state must not outlive the boot.
Examples
Section titled “Examples”# Make production builds take turns on this machineharn admission run --resource build -- npm run build
# Let two documentation renders overlap, but never a thirdharn 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 itharn admission run --detach --resource build -- npm run buildharn admission jobsharn admission wait /tmp/harnery-admission-jobs/jobs/<job-id>See also
Section titled “See also”harn qa-run: queues on thebrowser-qaresource by default, records the wait aswall_time_ms.queue, and turns an admission timeout into anincompleteresult with anadmissionblocker.harn qa-status:--queuerenders thebrowser-qaview of this same state.