Skip to content

Multi-host workers: one queue, one budget, sandboxes on several machines #57

Description

@edgehero

What

Two Mac minis (or Linux boxes) should be able to serve one pi-dispatch deployment: one queue, one budget, one admin view — sandboxes running on whichever host has capacity. The shape is a worker per machine sharing one Valkey, which the architecture already half-supports: BullMQ happily splits one queue across workers, and everything that lives in Valkey is already global. What's missing is everything that silently assumes one host.

What already works (don't redo this)

  • VALKEY_URL is remote-capable (host/port/auth/db all honored; the systemd unit even notes Valkey "may be remote"). Two workers on one queue split jobs correctly today.
  • All the shared state is already shared: jobId dedup and the semantic window are Redis-side; the budget windows and the daily token counter are atomic INCRs on shared keys ("verified against a real Valkey under 20 parallel increments"); the pause/resume kill switch, scheduler stall counters, and the dispatch_run hourly limit are all Valkey state. Two workers share one cap regime for free.
  • The boot reaper is per-daemon, not per-queuedocker ps --filter name=pi-job- only sees its own machine, so two workers on two machines don't kill each other's containers (the documented single-worker invariant is per docker daemon; co-located workers remain forbidden).
  • Container names embed globally-unique jobIds — no cross-host collision.

Gap 1 — divergent triggers files are a mutual-teardown loop

reconcile prunes every resident scheduler not named in this worker's config. Two workers with different triggers.json files each delete the other's schedulers on every boot and every file-watch reload. Idempotence only holds when the files are identical — and nothing checks that they are. Divergence must refuse loudly (e.g. a triggers-file hash in Valkey; mismatching worker refuses cron ownership) or reconcile must be owned by exactly one elected worker.

Gap 2 — local folders are host paths with no affinity

run.folder must exist on whichever host pops the job — but jobs land wherever BullMQ sends them. Worse, folder existence is validated at boot, so a worker whose host lacks one configured folder refuses to start entirely. Multi-host needs host affinity for local/cron work: name the host on the trigger and route (per-host queue or pop-time requeue), while github jobs stay any-host — their workspace is a fresh per-job clone.

Gap 3 — run history splits across hosts, invisibly

Each worker writes sidecars to its own logsDir; the admin reads exactly one, returns [] for anything missing, and shows a partial runs list with no hint that a second host exists. previousRunAt in a cron job's event.json scans the local dir only, so a scheduler alternating hosts reports the wrong previous fire. Records need a merged view. This grinds against DES-RUN-HISTORY-FLAT-FILES-NO-DB — state that tension openly: the least-new-machinery answer is mirroring the (already PII-free, already bounded) records into Valkey with a TTL, which is queue-state-shaped, not a database; the files stay the durable source on each host.

Gap 4 — per-host config drifts silently

The settings overlay, pause-windows file, and triggers file are all local files with per-host fs.watch; the admin writes to its own host's copy. Two hosts diverge silently on concurrency, caps, model, and quiet hours. Same medicine as Gap 1: one declared source of truth, or divergence detection that refuses instead of drifting.

Gap 5 — nothing knows which host did what

No host identity exists anywhere: run records carry no host field, log lines carry no host field, and getWorkers degrades to a bare count ("unknown" on providers without CLIENT SETNAME). Minimum viable: a worker name (env-declared, defaulting to hostname) stamped on log lines and run records (additive, nullable), shown in the runs list and RUN_DETAIL.

Gap 6 — image parity is unverified

pi-job:latest is a per-host local tag; doctor checks presence per host, and nothing notices two hosts running different digests — same flow, different behavior, undebuggable. Doctor (or the worker boot line) should surface the image digest so mismatch is at least visible.

Explicitly NOT proposed

  • One worker driving remote Docker daemons (DOCKER_HOST/contexts). DES-WORKER-ON-HOST's finding kills it: the docker CLI translates client-side host paths for bind mounts; a remote daemon doesn't — every /job, /workspace, and local-folder mount breaks. The superseded DES-JOB-FILES-VIA-VOLUME-SUBPATH is the record of that road.
  • Kubernetes or any orchestrator. Two Macs and a queue is the target, and BullMQ already is the scheduler.
  • Relaxing the one-worker-per-daemon invariant. Multi-host, not co-located workers.

Acceptance

Two machines, one Valkey: github jobs run on either host; a cron/local job only ever runs on the host that has its folder, and a missing-folder configuration fails that trigger's routing loudly instead of refusing worker boot for unrelated work; schedulers install once and divergent triggers files refuse rather than prune each other; the admin shows a merged runs view labeled by host, and cron previousRunAt is correct across hosts; budget/token caps and the kill switch remain global; doctor surfaces per-host image digests; a single-host deployment is byte-identical to today.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions