Skip to content

feat: moonraker websocket status worker#25

Merged
peterus merged 2 commits into
mainfrom
feat/moonraker-websocket-worker
Apr 9, 2026
Merged

feat: moonraker websocket status worker#25
peterus merged 2 commits into
mainfrom
feat/moonraker-websocket-worker

Conversation

@peterus

@peterus peterus commented Apr 8, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a long-running asyncio worker that holds a persistent WebSocket connection to each configured Moonraker printer, consumes status notifications, and updates PrintQueue entries in the database. This finally fixes the original problem: server-side print-end detection now works without relying on an open browser tab.

Builds on the `PrinterBackend` abstraction from #24.

Architecture

  • `core/services/moonraker_ws.py` — one websocket per printer with exponential reconnect backoff. Subscribes to `print_stats`, `virtual_sdcard`, `display_status`, `heater_bed`, `extruder`. Supports Moonraker oneshot-token auth via `/access/oneshot_token` when an API key is configured; unauthenticated otherwise.
  • `core/services/printer_status_sync.py` — pure sync event → DB logic. Handles `notify_history_changed` (primary job-end trigger) and `notify_status_update` (progress + fallback terminal detection). Progress writes throttled to 5 s per entry.
  • `core/management/commands/moonraker_worker.py` — process entry point. Enables SQLite WAL + `busy_timeout=5000`, runs an `asyncio.TaskGroup` with a reconciler task that polls `PrinterProfile` every `WORKER_RELOAD_INTERVAL` seconds (default 10 s) and spawns/cancels/restarts per-printer tasks on add/remove/change — live reload, no container restart needed on printer config changes.

Model & View Changes

  • `PrintQueue` gains `progress`, `status_updated_at`, `last_error` fields (migration `0016`).
  • `QueueCheckPrinterStatusView` no longer talks to Moonraker directly — it reads the worker-populated DB fields and exposes a `worker_stale` flag when `status_updated_at` is older than 60 s (or absent). Response shape stays backwards compatible for the existing `polling.js`.
  • `moonraker.get_job_status()` reuses a shared `map_klipper_state()` helper so the WS worker and REST client stay in sync.

Deployment

  • New docker-compose service `worker` using the same `release` image, entrypoint overridden to skip `migrate`/`collectstatic` (handled by `web`).
  • `requirements.txt`: `websockets>=14,<16`.

Test Plan

  • 65 unit tests across all affected modules pass locally (`test_services_printer_status_sync`, `test_services_moonraker_ws`, `test_services_moonraker`, `test_services_printer_backend`, `test_services_queue`, `test_management_moonraker_worker`).
  • `ruff format --check .` + `ruff check .` clean.
  • `makemigrations --check --dry-run` → no changes detected.
  • Manual e2e against real Klipper printer: start a short job, close the browser, verify the queue entry flips to `awaiting_review` and `completed_at` is populated without any tab being open.
  • Live reload test: start worker, add/edit/remove a printer in admin, verify logs show the corresponding spawn/cancel within ~10 s.
  • Reconnect test: kill Moonraker mid-print, verify backoff log and clean resume.
  • SQLite lock check: drive Gunicorn with curl while worker writes progress, verify no "database is locked" errors.

Out of Scope

  • Prometheus metrics / worker healthchecks
  • Admin UI for worker status
  • Django Channels push to browser (frontend keeps polling, just cheaper now)
  • Bambu / MQTT backends (separate PR when hardware is available)

🤖 Generated with Claude Code

Adds a long-running asyncio worker that holds a persistent WebSocket
connection to each configured Moonraker printer, consumes status
notifications, and updates PrintQueue entries in the database. This
finally makes server-side print-end detection work without relying on
an open browser tab.

Architecture:
- core/services/moonraker_ws.py: one websocket per printer with
  exponential reconnect backoff. Subscribes to print_stats,
  virtual_sdcard, display_status, heater_bed, extruder. Supports
  Moonraker oneshot-token auth via /access/oneshot_token when an API
  key is configured; connects unauthenticated otherwise.
- core/services/printer_status_sync.py: pure sync event -> DB logic.
  Handles notify_history_changed (primary job-end trigger) and
  notify_status_update (progress + fallback terminal detection).
  Progress writes are throttled to PROGRESS_WRITE_INTERVAL (5s) per
  entry to keep SQLite happy.
- core/management/commands/moonraker_worker.py: the process entry
  point. Enables SQLite WAL + busy_timeout, runs an asyncio.TaskGroup
  with a reconciler task that polls PrinterProfile every
  WORKER_RELOAD_INTERVAL seconds (default 10s) and spawns/cancels/
  restarts per-printer tasks on add/remove/change — live reload, no
  container restart needed on printer config changes.

Model & view changes:
- PrintQueue gains progress, status_updated_at, last_error fields
  (migration 0016).
- QueueCheckPrinterStatusView no longer talks to Moonraker directly.
  It reads the worker-populated DB fields and exposes a worker_stale
  flag when status_updated_at is older than 60s (or absent). Response
  shape stays backwards compatible for the existing polling.js.
- moonraker.get_job_status() now reuses a shared map_klipper_state()
  helper so the WS worker and the REST client stay in sync.

Deployment:
- New docker-compose service `worker` using the same release image,
  entrypoint overridden to skip migrate/collectstatic (web runs them).
- requirements.txt: add websockets>=14,<16.

Tests (65 total across affected modules):
- test_services_printer_status_sync: history_changed transitions,
  throttled progress writes, terminal state via notify_status_update,
  last_error population.
- test_services_moonraker_ws: build_ws_url and fetch_oneshot_token
  helpers.
- test_management_moonraker_worker: reconciler spawn/cancel/restart
  with a fake task group (IsolatedAsyncioTestCase).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 8, 2026 20:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a server-side Moonraker WebSocket worker that keeps printer status/progress up to date in the DB, enabling reliable print-end detection without relying on browser polling.

Changes:

  • Introduces a long-running moonraker_worker management command that maintains per-printer WebSocket connections and live-reloads printer config changes.
  • Adds PrintQueue.progress, PrintQueue.status_updated_at, and PrintQueue.last_error fields plus sync event→DB logic (printer_status_sync).
  • Updates the queue status polling endpoint to read from DB fields and emit a worker_stale hint instead of calling Moonraker directly.

Reviewed changes

Copilot reviewed 11 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
requirements.txt Adds websockets dependency needed for the Moonraker WS client.
docker-compose.yml Adds a dedicated worker service to run the management command.
core/views/queue.py Changes /api/queue/<pk>/check-status/ to serve worker-populated DB status/progress (+ staleness).
core/models/queue.py Adds progress, status_updated_at, last_error fields to PrintQueue.
core/migrations/0016_printqueue_progress_fields.py Migration adding the new PrintQueue fields.
core/services/moonraker.py Adds shared map_klipper_state() helper for consistent state normalization.
core/services/moonraker_ws.py Implements the Moonraker JSON-RPC WebSocket client + oneshot token helper.
core/services/printer_status_sync.py Adds sync notification handling + throttled progress writes + terminal transitions.
core/management/commands/moonraker_worker.py Adds the asyncio TaskGroup worker/reconciler process entry point and SQLite WAL tuning.
core/tests/test_services_printer_status_sync.py Unit tests for event→DB transition logic and progress throttling.
core/tests/test_services_moonraker_ws.py Unit tests for WS URL building and oneshot-token fetching behavior.
core/tests/test_management_moonraker_worker.py Tests for reconciler behavior (spawn/cancel/restart).

Comment thread core/management/commands/moonraker_worker.py
Comment thread core/services/moonraker_ws.py Outdated
Comment thread core/services/printer_status_sync.py
@github-actions

github-actions Bot commented Apr 8, 2026

Copy link
Copy Markdown

Preview image ready

By tag:

docker pull ghcr.io/peterus/layernexus:pr-25

By digest:

docker pull ghcr.io/peterus/layernexus@sha256:b57a343e90c4091aeed6570e3c6f928a51620966053a6d1d357fee0cc476843c

Commit: 650d80c8274fee6dcf449dbacd651fb20685755c

- moonraker_worker._main(): cancel all per-printer tasks in addition
  to the reconciler on shutdown. Without this, the TaskGroup context
  manager would hang waiting for printer tasks that never observe the
  stop_event, so SIGTERM/SIGINT wouldn't terminate the worker
  promptly. The active map is now shared between _main and the
  reconciler so _main can see and cancel spawned tasks.
- moonraker_ws.MoonrakerWebSocketClient: reset the reconnect backoff
  to RECONNECT_INITIAL_DELAY after every successful subscribe.
  Previously a transient outage could permanently inflate the
  reconnect sleep up to RECONNECT_MAX_DELAY. Stored as an instance
  attribute so _connect_and_listen can reset it from the happy path.
- printer_status_sync._transition_to_review(): clear last_error on
  successful completion so a stale failure from a previous retry
  doesn't leak into a later successful one. Added a regression test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@peterus peterus merged commit 3a39211 into main Apr 9, 2026
5 checks passed
@peterus peterus deleted the feat/moonraker-websocket-worker branch April 9, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants