What
Slice 2 of #216: vole runner --jobs N.
- N slots in one process. Each owns a session and runs at most one job; the
server already leases per session and claims it with a conditional update, so
nothing on that side changed.
Options.Jobs,--jobs(default 1,< 1refused),RUNNER_JOBSin the first-boot renderer. - Process work happens once. Directories, cache bounds, the leftover sweep,
the connections, and the
--reposcheck are done before any slot starts. The slots share the cache root, the saved checkouts and the images; they differ only in the session they lease on. - Failure policy. A job this runner refuses stops every slot and the process
reports why — reconnecting would ask the same instance the same question. A lost
connection is the slot’s own: it reconnects with a doubling delay (2s → 30s) and
the job it was running is requeued when its lease expires.
--oncekeeps the one-shot shape: a failure ends the process, and the scheduler decides about a retry. - The log stream belongs to the job. It was a field on the session
(
s.stream), which only worked because a session never had two jobs. It is now created inrunand passed tofinish. - A halted slot stops leasing. A slot that is told to stop finishes the job it is on and reports it, then exits.
Tests
TestSeveralSessionsLeaseDifferentJobs: four sessions on one account lease four distinct runs, each on attempt 1. (Writing it found thatenqueueis idempotent per(repo, ref, sha, workflow, job), so four identical enqueues are one run — the test varies the job name and says why.)TestARepositoryAtItsCapLeavesTheSlotsForOthers:max_jobs_per_repo = 2with three sessions leases exactly two.TestASlotRefusesAJobItWillNotRun/TestAnEmptyLeaseIsNotARefusal: the refusal/reconnect classification, over a pipe instead of a server.TestNextDelayDoublesAndCaps,TestSleepStopsWhenTheContextEnds.TestRunnerRejectsAJobCountBelowOne:0,-1and a non-number are refused before anything connects.
Verification
gofmt -l ., go vet ./..., go test -tags sqlite_fts5 ./... clean, and
vole workflow run verify ok (format, vet, build, licenses, govulncheck,
packages, deployment files, links, go test -race -count=1 ./...). Also
scripts/check-deploy.py against the rendered first-boot file, which now asserts
the job count survives rendering.
Deliberately not here
- SIGTERM does not drain. Today the process is killed mid-step; with slots that
is N jobs instead of one. Draining needs
signal.NotifyContextplus a way to cut a parked lease short, and a second signal has to still kill it — worth its own change, and the one thing I would do next. - The image-build singleflight (two slots building the same
image-filedigest) is slice 3, with the remaining docs. - Presence is N rows per machine on the runners page, and an idle slot holds a
connection against
ssh_max_connections. Both are documented; grouping rows by machine is a UI decision, not a blocker.
Docs
docs/runner.md gains “Running several jobs at once” (session per slot, per-job
container limits, /cache shared with the same repository’s own jobs, connections),
docs/cli.md and packaging/runner.env explain the flag, docs/deploy/runner.md
covers RUNNER_JOBS.
Refs #216 (does not close it).