@@ -72,6 +72,86 @@ flowchart LR
7272
7373Read [ ` SECURITY.md ` ] ( SECURITY.md ) before you rely on it — it states plainly what is and is not defended.
7474
75+ ## Run as a service
76+
77+ ` pi-dispatch worker ` is a long-running process — run it in a terminal, or hand it to your OS's service
78+ manager so it starts on boot and restarts on a crash. The units in [ ` deploy/ ` ] ( deploy/ ) are ** per-host
79+ templates, not turnkey** : each carries ` <PLACEHOLDER> ` paths you fill in for your machine. The systemd
80+ unit's * structure* is checked by ` systemd-analyze ` ; the launchd and nssm units are worked examples. All
81+ three run the worker on the ** host** — it drives the ` docker ` CLI and is not itself containerised — so
82+ they need the AOF-enabled Valkey from [ ` deploy/docker-compose.yml ` ] ( deploy/docker-compose.yml ) running
83+ alongside, which is what makes the queue ** and the pause state** survive a reboot.
84+
85+ ** Steer the running worker** without stopping it — these commands talk to Valkey, so they work whether the
86+ worker runs in a terminal or under a service manager:
87+
88+ - ` pi-dispatch pause ` — stop taking new jobs. ** Durable** : the pause lives in the queue and survives a
89+ worker restart, so a paused worker comes back paused after a reboot. Jobs still enqueue; they just wait.
90+ - ` pi-dispatch resume ` — start taking jobs again.
91+ - ` pi-dispatch status ` — prints ` { pausedState, waiting, active, paused, delayed, failed } ` . ` pausedState `
92+ is the switch; ` paused ` is the backlog ** count** of jobs that piled up while paused (they land in the
93+ ` paused ` list, not ` waiting ` ).
94+
95+ ### Linux (systemd)
96+
97+ Edit [ ` deploy/worker.service ` ] ( deploy/worker.service ) : set ` WorkingDirectory ` , ` EnvironmentFile ` , ` User ` ,
98+ and the ` node ` path to your clone. Then install and start it:
99+
100+ ``` bash
101+ sudo cp deploy/worker.service /etc/systemd/system/
102+ sudo systemctl daemon-reload
103+ sudo systemctl enable --now worker
104+ ```
105+
106+ ` systemctl stop worker ` sends ** SIGTERM** — the worker stops accepting jobs and lets the in-flight
107+ container drain before it exits.
108+
109+ ### macOS (launchd)
110+
111+ Edit [ ` deploy/com.pi-dispatch.worker.plist ` ] ( deploy/com.pi-dispatch.worker.plist ) and its wrapper
112+ [ ` deploy/worker-env-wrapper.sh ` ] ( deploy/worker-env-wrapper.sh ) : set the repo-root and log paths (launchd
113+ has no ` EnvironmentFile ` , so the wrapper loads ` .env ` at runtime). Then bootstrap it:
114+
115+ ``` bash
116+ launchctl bootstrap gui/$( id -u) deploy/com.pi-dispatch.worker.plist
117+ ```
118+
119+ ` launchctl bootout gui/$(id -u)/com.pi-dispatch.worker ` sends ** SIGTERM** for the same graceful drain.
120+
121+ ### Windows (nssm)
122+
123+ Put ` nssm.exe ` on PATH ([ nssm.cc] ( https://nssm.cc ) ), set ` SERVICE ` / ` REPO ` / ` LOGDIR ` in
124+ [ ` deploy/nssm-install.cmd ` ] ( deploy/nssm-install.cmd ) , then run it and start the service:
125+
126+ ```
127+ deploy\nssm-install.cmd
128+ nssm start pi-dispatch-worker
129+ ```
130+
131+ The wrapper [ ` deploy/worker-env-wrapper.cmd ` ] ( deploy/worker-env-wrapper.cmd ) loads ` .env ` at runtime. A
132+ console-stop (` nssm stop pi-dispatch-worker ` ) sends the worker a signal it handles, so it drains
133+ gracefully.
134+
135+ ### Drain before a planned restart
136+
137+ A planned restart should abort no in-flight job. Pause, wait for the queue to go idle, restart, then
138+ resume:
139+
140+ ``` bash
141+ pi-dispatch pause # stop taking new jobs (durable)
142+ pi-dispatch status # repeat until "active": 0 — nothing in flight
143+ sudo systemctl restart worker # (or the launchctl / nssm equivalent)
144+ pi-dispatch resume # take jobs again
145+ ```
146+
147+ Because the pause is durable, the worker comes back paused even if the restart outruns your ` resume ` , so
148+ nothing slips through in the gap.
149+
150+ ** Windows caveat** : stop the service with nssm's ** console-stop** (` nssm stop ` ), which delivers a signal
151+ the worker handles and drains gracefully. Task Scheduler is a weaker fallback — it stops a task with a
152+ hard kill, giving the worker no chance to drain; a job killed mid-flight leaves a stray container that the
153+ worker's ** boot reaper** clears on the next start, rather than draining cleanly.
154+
75155## Advanced: GitHub automation
76156
77157pi-dispatch can also be triggered by GitHub — label an issue, and a container works it on a fresh clone,
@@ -133,7 +213,8 @@ minutes.
133213## Status
134214
135215The local-folder path (image, worker, ` pi-dispatch run ` / ` worker ` ) and the GitHub webhook path
136- (receiver → queue → clone → PR) are built and work. The admin panel and scheduled (cron) triggers are in
216+ (receiver → queue → clone → PR) are built and work; the worker runs in a terminal or as an OS service on
217+ Linux, macOS or Windows (see ** Run as a service** ). The admin panel and scheduled (cron) triggers are in
137218progress. The design is specified in
138219[ ` specs/ ` ] ( specs/ ) — start with [ ` specs/constitution.md ` ] ( specs/constitution.md ) for the non-negotiables
139220and [ ` specs/design.md ` ] ( specs/design.md ) for the decisions and what was rejected.
0 commit comments