Configuration
Every environment variable a Runner reads, with its default, and the ones that fail in ways nothing else warns about.
A Runner is configured entirely by environment variables. There is no configuration file and no command line.
Every variable is prefixed AJ_, with __ between sections — the same
convention the Server reads, so one Compose file does not have to spell the two
services differently.
The Server
| Variable | Default | |
|---|---|---|
AJ_Server__BaseUrl | required | The Server's API root, including /api/v1 |
The prefix is checked at start: a base URL with no /api/ is refused by name
rather than discovered as a wall of 404s. The Server's path guard answers those
with an empty body, so there would be nothing to read.
Identity and capabilities
| Variable | Default | |
|---|---|---|
AJ_Runner__Name | $HOSTNAME, else algojudge-runner | The name shown in the manager panel |
AJ_Runner__KeyPath | /var/lib/algojudge-runner/identity.key | The Ed25519 key. Generated on first start; put it on a volume |
AJ_Runner__ProblemTypes | standard-io@1 | Comma-separated. What this Runner offers to evaluate |
AJ_Runner__Tags | empty | Comma-separated pools. A seed, honoured at the first registration only — see Tags and routing |
AJ_Runner__ProblemTypes is how a Runner that should also take output-only@1
work is told to: standard-io@1,output-only@1. Which types a Runner advertises
is configuration, not code.
The default in that table is the binary's, for a process started with the
variable unset. AlgoJudge-Ops ships RUNNER_PROBLEM_TYPES=standard-io@1,output-only@1,
so an installation's Runners declare both — which is why a queue that looks stuck
is diagnosed against the value the service actually has and not against this
column.
Polling and the lease
| Variable | Default | |
|---|---|---|
AJ_Poll__WaitSeconds | 25 | How long the Server may hold a claim open on an empty queue. 0 asks for none |
AJ_Poll__MinSeconds | 1 | Shortest wait after an empty queue |
AJ_Poll__MaxSeconds | 30 | Longest wait. The backoff is jittered |
AJ_Heartbeat__Seconds | 60 | How often an idle Runner says it is alive |
AJ_Lease__RequestSeconds | 600 | How long a claim asks to hold a job for |
AJ_Poll__WaitSeconds makes the two below matter only after a failure. On
an empty queue the Server holds the request rather than answering it, so the
wait is the interval, and a submission starts being judged as soon as it
exists instead of at the next poll. See Claiming for
what the Server does with it.
What bounds it in practice is not this product but whatever sits between the
Runner and the Server — an Azure Application Gateway ends a silent request at
20 s, a stock nginx or an AWS ALB at 60 s, Cloudflare at 125 s. Twenty-five
clears all but the first. Raise it on a path you own; lower it, or set 0,
behind a proxy you do not control.
Three hundred is a ceiling, and above it the Runner refuses to start. That is the longest a Server will hold a claim, and asking for more is worse than being ignored: the Runner tells a held claim from an immediate answer by how long it took, so one asking for nine hundred would read the Server's three hundred as no wait at all and sleep the interval below after every claim — the deafness the held claim exists to remove, restored by a setting and visible in no log.
AJ_Lease__RequestSeconds is a request. The Server clamps it to
[60, 3600] and answers with the deadline it actually granted, which is the
authoritative one. The lease is renewed automatically while work is in progress,
on a timer at a quarter of the granted lease — three renewals fit inside every
lease, so two may fail in a row with the deadline still ahead.
The heartbeat is sent only when the Runner has nothing to do. A Runner holding a job is renewing its lease instead, which says the same thing more precisely.
Storage
| Variable | Default | |
|---|---|---|
AJ_Cache__Path | /var/cache/algojudge-runner | The package cache |
AJ_Cache__MaxBytes | 10737418240 (10 GiB) | Cache ceiling |
AJ_Work__Path | /var/lib/algojudge-runner/work | Scratch, as this process sees it |
AJ_Work__HostPath | the value of AJ_Work__Path | Scratch, as the container runtime's daemon sees it |
AJ_Pipes__Path | unset | Where a judged run's channels are made, when the work directory cannot hold them. Unset leaves them in the job's own scratch |
AJ_Pipes__HostPath | the value of AJ_Pipes__Path | The same directory as the daemon sees it. Set both or neither |
The package cache is the only state a Runner keeps besides its key. Losing it costs a re-download, nothing more.
The pipes pair is not about space: a judged run's output travels on a named
pipe, which holds no bytes at all, so what it needs is a filesystem that can
make one. A bind mount of a Windows or macOS directory cannot — a developer's
checkout, not a deployment. A tmpfs: entry on the Runner's own service will
not do either: that is private to the Runner's mount namespace, and the daemon
would make an empty directory instead, which is the failure the callout below
describes.
AJ_Work__HostPath is the path the Docker daemon can open, not the path the Runner sees
A bind mount is resolved by the container runtime's daemon. When the Runner is itself in a container, the path it sees is not a path the daemon can open — and a path the daemon cannot open produces an empty directory rather than an error.
The failure that follows is silent and looks like nothing at all: tests run against an empty mount, and every submission is judged against no input.
Where the Runner is not containerised the two paths are the same and this can
be left alone. Where it is, AJ_Work__HostPath must name the host directory that
is bind-mounted at AJ_Work__Path.
The sandbox and the language images
| Variable | Default | |
|---|---|---|
AJ_Sandbox__AllowUnmeasured | unset | Start anyway on a host this Runner cannot measure on — and only start: every job it claims then fails |
AJ_Sandbox__AllowCgroupV1 | unset | The old name for the row above, still read |
AJ_Sandbox__CgroupRoot | /sys/fs/cgroup | Where the cgroup filesystem is mounted. Must be absolute; empty is treated as unset |
AJ_Sandbox__Image__Gcc | algojudge/lang-gcc:local | |
AJ_Sandbox__Image__Clang | algojudge/lang-clang:local | |
AJ_Sandbox__Image__Python | algojudge/lang-python:local | |
AJ_Sandbox__Image__Pypy | algojudge/lang-pypy:local | |
AJ_Sandbox__Image__Cpp | — | The old name for AJ_Sandbox__Image__Gcc, still read. The new name wins where both are set |
Each image is independent: anything left unset keeps its compiled-in default, so an operator republishing one image says so in one line. The defaults are what a development stack builds locally, so a deployment names all four explicitly.
AJ_Sandbox__Image__Gcc=ghcr.io/algojudge/lang-gcc:0.1.0
AJ_Sandbox__Image__Clang=ghcr.io/algojudge/lang-clang:0.1.0
AJ_Sandbox__Image__Python=ghcr.io/algojudge/lang-python:0.1.0
AJ_Sandbox__Image__Pypy=ghcr.io/algojudge/lang-pypy:0.1.0Pin the same version as the Runner unless there is a reason not to: that pairing is what the release was tested as. Languages says why the compiler version is a thing to pin rather than a thing that moves.
AJ_Sandbox__AllowUnmeasured is on whenever it holds any value at all
It is read as set or not set, not as a boolean. Any non-empty value turns it
on — including AJ_Sandbox__AllowUnmeasured=false, which reads as allow. The
same is true of AJ_Sandbox__AllowCgroupV1, which is the old name for it.
To turn it off, remove the variable or leave it empty.
It exists for a development machine whose Docker still reports v1, and it shouts
at ERROR on every start: "STARTING BELOW SPECIFICATION". Times and memory
reported beside a verdict on such a host are not to be trusted.
Isolation says why.
The variables that are not ours
These are read by the libraries the Runner is built on rather than by its own
configuration. They carry no AJ_ prefix, and the Runner never validates them.
| Variable | |
|---|---|
RUST_LOG | The log filter, in the usual tracing grammar. Unset, the Runner logs at info; a malformed filter falls back to info rather than refusing to start |
DOCKER_HOST | Where the container runtime's socket is. Only a unix://… value is honoured — anything else is ignored in silence, and the Runner goes on using the local default |
NO_COLOR | Set to anything to drop the colour from the log output |
HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY | The usual proxy configuration. A proxy URL may carry a credential, and one written here appears in the process listing |
Two variables to leave alone
Neither is ours, both are read by a library, and each breaks the Runner in a way that looks like something else.
REQUEST_METHOD — any value, an empty one included, makes the HTTP client
believe it is running as CGI, and it then discards the whole proxy
configuration, HTTPS included.
TOKIO_WORKER_THREADS — an empty value, or 0, panics inside the async
runtime before anything of ours runs.
Three ways a configuration fails
A malformed number panics at start. Every numeric variable above is parsed
strictly, and AJ_Poll__MaxSeconds=3O — a letter O for a zero — stops the
process with a message naming the key and the value. There is deliberately no
fallback to the default: an operator whose setting did nothing would otherwise
never learn it.
A wrong AJ_Work__HostPath does not fail at all. It is the opposite case and
the reason the callout above exists.
And two settings are accepted and then quietly do the wrong thing. The poll
interval is advisory rather than checked, so an AJ_Poll__MaxSeconds below
AJ_Poll__MinSeconds is silently raised to meet it instead of being refused. And
a typo in AJ_Runner__ProblemTypes is accepted at start-up — the Runner declares
the misspelt type, is handed nothing that matches it, and looks exactly like a
Runner nobody has any work for.
A value naming no type at all is a different case and is refused by name: a lone comma, or a trailing one from a paste, leaves the list empty, and an empty list matches no problem in the installation. That Runner would register, be approved, heartbeat, show as connected and be handed nothing for as long as it ran.
An empty AJ_Sandbox__CgroupRoot is not one of them: it is treated as unset,
and a relative value is refused by name.
Everything else that can be wrong shows up as a queue that never drains. When that happens, read Tags and routing first.