PH0.3 — Worker skeleton: claim loop, task handler registry, jobs discovery, heartbeat #6

Closed
opened 2026-09-11 09:02:08 +00:00 by bart · 4 comments
Collaborator

Parent

Row PH0.3 in docs/12-implementation-plan.md (branch, owned paths and doc section are defined there).

What to build

The worker process claims rows from the task table with FOR UPDATE SKIP LOCKED, dispatches them to handlers registered by decorator and found by package discovery, retries with backoff, runs discovered scheduled jobs, and logs a heartbeat. Domain code can enqueue work through a single helper.

Acceptance criteria

  • Worker starts against the compose stack and logs a heartbeat on schedule
  • A task enqueued via the domain helper is claimed exactly once by one of two concurrent workers and its handler runs
  • A failing handler is retried with backoff and ends in a failed state after the retry budget; state changes are visible on the task row
  • A handler module dropped into the handlers package is registered without editing a shared file; same for a job module
  • Unit tests cover claim, retry and registry; ruff/mypy/pytest green

Blocked by

  • #4 — API skeleton: core, router discovery, healthz, db + audit hook, alembic 0001, CLI
## Parent Row **PH0.3** in `docs/12-implementation-plan.md` (branch, owned paths and doc section are defined there). ## What to build The worker process claims rows from the `task` table with `FOR UPDATE SKIP LOCKED`, dispatches them to handlers registered by decorator and found by package discovery, retries with backoff, runs discovered scheduled jobs, and logs a heartbeat. Domain code can enqueue work through a single helper. ## Acceptance criteria - [x] Worker starts against the compose stack and logs a heartbeat on schedule - [x] A task enqueued via the domain helper is claimed exactly once by one of two concurrent workers and its handler runs - [x] A failing handler is retried with backoff and ends in a failed state after the retry budget; state changes are visible on the task row - [x] A handler module dropped into the handlers package is registered without editing a shared file; same for a job module - [x] Unit tests cover claim, retry and registry; ruff/mypy/pytest green ## Blocked by - #4 — API skeleton: core, router discovery, healthz, db + audit hook, alembic 0001, CLI
bart self-assigned this 2026-09-11 10:28:46 +00:00
Author
Collaborator

Starting on branch feature/worker-skeleton (from develop @ PH0.2 merge). Owned paths: backend/src/polaris/worker/**, backend/tests/unit/worker/**, plus backend/src/polaris/domain/tasks.py (creating the domain package with only this file). Minimal, explicitly-scoped extension to backend/src/polaris/db/repositories/task.py for the TaskRepository implementation the import rules require (per the ticket brief). Also adding backend/tests/unit/domain/** since that dir doesn't exist yet and pairs with domain/tasks.py.

Two notes:

  1. Stopwatch: current CLAUDE.md on main warns the Forgejo stopwatch is one clock per account, shared across parallel sessions on this token, and says only start it if no other session currently has one open — I can't confirm that from here, so I'm skipping stopwatch start/stop for this ticket rather than risk stomping another session's timer.
  2. Seam note re: polaris worker CLI commandbackend/Dockerfile (PH0.2) already comments that ENTRYPOINT ["polaris"] / CMD ["api"] expects a worker subcommand from this ticket, and docs/07-architecture.md's compose table has the worker service running polaris worker. That's a cli/worker.py file (~15 lines, same shape as cli/api.py), but my brief says not to touch anything under backend/src/polaris/cli/**. I'm building the actual entrypoint as polaris.worker.main:main() (also reachable via python -m polaris.worker) so a future cli/worker.py is a trivial wrapper — flagging this as a seam request rather than widening this ticket to touch cli/.
Starting on branch `feature/worker-skeleton` (from `develop` @ PH0.2 merge). Owned paths: `backend/src/polaris/worker/**`, `backend/tests/unit/worker/**`, plus `backend/src/polaris/domain/tasks.py` (creating the `domain` package with only this file). Minimal, explicitly-scoped extension to `backend/src/polaris/db/repositories/task.py` for the `TaskRepository` implementation the import rules require (per the ticket brief). Also adding `backend/tests/unit/domain/**` since that dir doesn't exist yet and pairs with `domain/tasks.py`. Two notes: 1. **Stopwatch**: current `CLAUDE.md` on `main` warns the Forgejo stopwatch is one clock per account, shared across parallel sessions on this token, and says only start it if no other session currently has one open — I can't confirm that from here, so I'm skipping stopwatch start/stop for this ticket rather than risk stomping another session's timer. 2. **Seam note re: `polaris worker` CLI command** — `backend/Dockerfile` (PH0.2) already comments that `ENTRYPOINT ["polaris"]` / `CMD ["api"]` expects a `worker` subcommand from this ticket, and `docs/07-architecture.md`'s compose table has the `worker` service running `polaris worker`. That's a `cli/worker.py` file (~15 lines, same shape as `cli/api.py`), but my brief says not to touch anything under `backend/src/polaris/cli/**`. I'm building the actual entrypoint as `polaris.worker.main:main()` (also reachable via `python -m polaris.worker`) so a future `cli/worker.py` is a trivial wrapper — flagging this as a seam request rather than widening this ticket to touch `cli/`.
bart removed their assignment 2026-09-11 10:33:17 +00:00
Author
Collaborator

Filed the CLI seam request as #52 (cli/worker.py needed for docker compose's worker service command: ["polaris", "worker"] to actually start it). The Forgejo issue-dependency API returned 404 on this instance so I couldn't wire a formal blocked-by edge — noting the relationship here instead: #52 blocks acceptance criterion 1 of this ticket ("Worker starts against the compose stack...") being provable end-to-end; everything else in this ticket (claim loop, retry/backoff, handler/job discovery, heartbeat job, unit tests) is self-contained and does not depend on #52.

Filed the CLI seam request as #52 (`cli/worker.py` needed for `docker compose`'s `worker` service `command: ["polaris", "worker"]` to actually start it). The Forgejo issue-dependency API returned 404 on this instance so I couldn't wire a formal *blocked-by* edge — noting the relationship here instead: #52 blocks acceptance criterion 1 of this ticket ("Worker starts against the compose stack...") being provable end-to-end; everything else in this ticket (claim loop, retry/backoff, handler/job discovery, heartbeat job, unit tests) is self-contained and does not depend on #52.
Author
Collaborator

PR opened: #53 (targets main, per the corrected git model). 4/5 acceptance criteria checked above; AC1 ("Worker starts against the compose stack...") is blocked on seam request #52 (cli/worker.py) — everything it needs on the worker side (discovery, scheduler, heartbeat job) is done and tested, confirmed working inside the built Docker image via python -m polaris.worker.

PR opened: https://git.xor.nu/patrick/Polaris/pulls/53 (targets `main`, per the corrected git model). 4/5 acceptance criteria checked above; AC1 ("Worker starts against the compose stack...") is blocked on seam request #52 (`cli/worker.py`) — everything it needs on the worker side (discovery, scheduler, heartbeat job) is done and tested, confirmed working inside the built Docker image via `python -m polaris.worker`.
bart closed this issue 2026-09-11 11:13:31 +00:00
Author
Collaborator

Last AC ticked. While drafting the seam PR for #52 (cli/worker.py), I actually ran the worker against the real compose stack (not just unit tests) and found two more bugs blocking this: the compose worker service's command: duplicated the Dockerfile's ENTRYPOINT, and POLARIS_DB_URL isn't a real Settings field (silently ignored -- affected api too). Fixing those surfaced a third, deeper bug: task.py's task_status enum column persisted the Python member name (QUEUED) instead of .value (queued), which the real Alembic-migrated schema uses -- masked by conftest.py building its test schema from the same buggy column definition via create_all(). All three fixed and verified end-to-end (real image, real migration, real heartbeat) in #64.

Last AC ticked. While drafting the seam PR for #52 (cli/worker.py), I actually ran the worker against the real compose stack (not just unit tests) and found two more bugs blocking this: the compose worker service's `command:` duplicated the Dockerfile's `ENTRYPOINT`, and `POLARIS_DB_URL` isn't a real `Settings` field (silently ignored -- affected `api` too). Fixing those surfaced a third, deeper bug: `task.py`'s `task_status` enum column persisted the Python member name (`QUEUED`) instead of `.value` (`queued`), which the real Alembic-migrated schema uses -- masked by `conftest.py` building its test schema from the same buggy column definition via `create_all()`. All three fixed and verified end-to-end (real image, real migration, real heartbeat) in #64.
Sign in to join this conversation.
No labels
ready-for-agent
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
patrick/Polaris#6
No description provided.