slotgate 0.1.0

A bounded-parallelism job runner that gives each slot its own disjoint port range, so port-binding tests run in parallel without collisions.
Documentation

slotgate

crates.io license

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

cargo install slotgate

How it works

  • You provide a --program, a list of --jobs, and --program-args containing the literal token {job}.
  • For each job, slotgate runs <program> <program-args> with {job} substituted for the job name, in one of --max-parallel slots.
  • 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_BASE and PORT_RANGE_COUNT by 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.log under --log-dir.
  • The process exits 0 only if every job passed; a failure or timeout exits non-zero.

Usage

slotgate \
  --program target/debug/deps/all_tests-<hash> \
  --program-args '{job},--exact,--nocapture' \
  --jobs first_test,second_test,third_test \
  --max-parallel 4 \
  --port-range-base 40000 \
  --port-range-size 100

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 = std::env::var("PORT_RANGE_BASE").unwrap().parse().unwrap();
let count: u16 = std::env::var("PORT_RANGE_COUNT").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:

slotgate \
  --pre-build-program cargo \
  --pre-build-args 'test,--no-run,--message-format=json' \
  --pre-build-target-name all_tests \
  --jobs first_test,second_test \
  --program cargo --program-args '{job}'

--program is still required by the CLI even when discovery overrides it — pass any placeholder.

License

MIT — see LICENSE.