jr
2026A local distributed job runner and scheduler in C++20: one scheduler, N workers, one CLI, zero dependencies.
jr is a local distributed job runner: a small compute-cluster-in-a-binary
built on nothing but C++20, POSIX sockets, and threads. One scheduler
process owns the queue and tracks the worker fleet; worker daemons run
tasks and report their capacity; a single jr binary is the CLI for all
of it — submit, inspect, stream logs, drain nodes. The question behind
the project: how much of a real cluster's behavior can you get in ~2,000
lines with no dependencies? The answer is most of it — dependency DAGs,
retries with backoff, placement constraints, live log streaming, crash
recovery, and hard resource limits are all here.
One binary, three roles.
The same executable runs as scheduler, worker, or CLI depending on its
first argument. The scheduler (schedd) is the only stateful component —
it holds the job queue, the worker registry, and every dependency edge.
Workers are dumb: they register with a capacity, receive assignments, run
fork+execvp, and report results. The CLI is thinner still — it opens
a TCP connection, sends one frame, prints the reply.
A wire protocol with no serialization library.
Every message on the wire is a length-prefixed TCP frame: a big-endian
u32 length, a u8 type tag, then a payload of fields joined by 0x1f
and inner lists by 0x1e. Eighteen message types cover the whole system.
No protobuf, no codegen — parsing is a split on a byte value, and the
entire codec is ~50 lines.
| Direction | Messages |
|---|---|
| worker → scheduler | Register, Heartbeat, TaskResult, LogData |
| cli → scheduler | Submit, ListJobs, JobInfo, CancelJob, ListWorkers, Stats, JobState, DrainWorker, SubLogs |
| scheduler → worker | RegAck, Assign, Kill, LogSub |
| scheduler → cli | Reply, Error, LogChunk, LogEnd |
A job's life, and the ways it can fail.
Dispatch is a sorted scan: queued jobs in (priority, submit-time) order
get best-fit placement onto workers — the least free CPUs that still fit,
honoring --require label constraints. A job that doesn't fit anywhere
never blocks smaller jobs behind it. --after dependencies hold a job in
blocked until every dependency is done; because ids are minted at
submit time, dependency cycles are unconstructible rather than detected.
Failures are handled the way a real cluster would. Workers heartbeat every two seconds; a worker silent for six is marked lost and its running tasks' jobs are requeued, subject to the per-job retry cap.
Two more mechanisms keep the failure surface honest. --limit turns the
scheduler's memory bookkeeping into a hard cap: the request becomes
RLIMIT_AS on the child process. And kill is a process-group SIGKILL
— setsid on the child at spawn — so a cancelled job takes its whole
subtree down with it. jr drain marks a worker draining: running tasks
finish, nothing new is assigned, and undrain resumes it.
Persistence is a text file.
Scheduler state lives in an append-only line journal — S records for
submits, T records for state transitions (including run count, exit
code, and a note), and a W watermark pinning the latest id. Replay it
on startup and the entire queue is rebuilt; jobs that were running are
requeued rather than lost. The journal is compacted on clean shutdown
and every ~5,000 records.
jr logs -f streams live output through the same plumbing: the worker
tails the task's spool file and relays chunks through the scheduler to
subscribed clients, and the last 48 KB are stored with every result so
jr info always has a tail to show.
Scope.
The protocol is trusted-network — no auth or encryption yet, so it's for
localhost and private LANs. Dispatch is O(queued × workers), fine to
roughly 10⁴ jobs. RLIMIT_AS counts virtual address space, so mmap-heavy
runtimes need a larger --mem than their RSS suggests. Within those
bounds, the goal was a scheduler you can read end-to-end in an afternoon
— and then actually use.