slotgate
A bounded-parallelism job runner that gives each concurrency slot its own
disjoint port range. Jobs that bind ports — cluster tests, servers, anything
that opens sockets — run in parallel without colliding, so you get the speed of
parallel execution without falling back to #[serial] or a single-threaded run.
It is domain-agnostic: it runs <program> <program-args> once per job, and
knows nothing about what the job actually does.
Why
Tests that bind network ports can't safely share the machine when run in
parallel — two jobs grabbing the same port flake. The usual fixes are to
serialize them (slow) or to hand-tune port offsets (fragile). slotgate instead
partitions the port space into one disjoint range per slot and hands each job
its slot's range through environment variables. Concurrent jobs are guaranteed
non-overlapping ports, so they can all run at once.
Install
How it works
- You provide a
--program, a list of--jobs, and--program-argscontaining the literal token{job}. - For each job,
slotgateruns<program> <program-args>with{job}substituted for the job name, in one of--max-parallelslots. - Slot i owns the port range
[base + i*size, base + i*size + size). The job process receives its slot's range through two environment variables (PORT_RANGE_BASEandPORT_RANGE_COUNTby default) — the job binds ports from that window. Concurrently-running jobs therefore never share a port. - Each job has a per-job timeout and writes
stdout.log/stderr.logunder--log-dir. - The process exits
0only if every job passed; a failure or timeout exits non-zero.
Usage
Each job here runs the compiled test binary against a single test name, in a
slot whose 100-port window is exported as PORT_RANGE_BASE / PORT_RANGE_COUNT.
Options
| Flag | Default | Description |
|---|---|---|
--jobs |
(required) | Comma-separated job names |
--program |
(required) | Program to run once per job |
--program-args |
"" |
Comma-separated args; every {job} is replaced with the job name |
--max-parallel |
3 |
Maximum jobs running at once |
--port-range-base |
30000 |
First port of slot 0's range |
--port-range-size |
100 |
Ports per slot |
--port-env-base |
PORT_RANGE_BASE |
Env var carrying the slot's base port |
--port-env-count |
PORT_RANGE_COUNT |
Env var carrying the slot's port count |
--timeout-secs |
120 |
Per-job timeout |
--log-dir |
logs/slotgate |
Root for per-job stdout.log / stderr.log |
--pre-build-program |
(none) | One-time setup command run before any job (the run aborts if it fails) |
--pre-build-args |
"" |
Comma-separated args for the pre-build command |
--pre-build-target-name |
(none) | See below |
Reading the port range in a job
A job binds ports inside [PORT_RANGE_BASE, PORT_RANGE_BASE + PORT_RANGE_COUNT).
For example, in Rust:
let base: u16 = var.unwrap.parse.unwrap;
let count: u16 = var.unwrap.parse.unwrap;
// bind within base .. base + count
The variable names are configurable with --port-env-base / --port-env-count.
Pre-build discovery (optional)
Building the test binary inside each job would cause build-lock contention. Run
the build once up front instead. If the pre-build command emits Cargo JSON
(--message-format=json) and you pass --pre-build-target-name, slotgate
finds the matching compiler artifact and uses that executable as the effective
--program for every job (with standard libtest args), so you don't have to
hardcode the hashed binary path:
--program is still required by the CLI even when discovery overrides it — pass
any placeholder.
License
MIT — see LICENSE.