fix: a lane that is OFF was not attributable to itself (4295)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 25s
CI and images / backend-lint-and-test (push) Successful in 36s
CI and images / integration (push) Successful in 2m26s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 1m39s
CI and images / smoke-web (push) Successful in 56s
CI and images / promote (push) Skipped

Operator, 2026-09-23: *"clean up the stale service_seen rows"*.

**They were not stale.** They were phantoms, written on purpose, and they will
come back on every install that turns a lane off — so the rows are the smaller
half of this.

A celery worker was attributed to its lane by the queues it was CONSUMING. A
lane at cap 0 has its consumers cancelled, so it answers `active_queues()`
with an empty list, matches no lane, and is dropped. Three consequences, all
on the operator's screen at once:

1. The lanes table reported the lane **not answering** — the signal for a
   crashed worker, not for one the operator turned off.
2. The roster grew a phantom row named **`Worker ()`** — the empty queue set
   rendered as a display name — shown "running" beside the real lane's row
   going stale, because nothing updated it any more.
3. **The container went unhealthy.** `healthcheck._lanes_ok` requires every
   lane present. ML ships at cap 0, so a fresh install was permanently
   unhealthy and Swarm restarts an unhealthy task forever.

That third one is the severe one, and its docstring asserted the opposite of
what the code did — *"a disabled lane still runs its process with its
consumers cancelled, so it answers inspect and is healthy"*. It answers. It
was not attributed. A comment can be right about the intent and wrong about
the program, and this one had been wrong since the consolidated container
shipped.

`worker_lanes.lane_for_node` attributes by NODE NAME instead: identity travels
with the process rather than with what it happens to be doing.
`gen_supervisord` already sets `CELERY_NODENAME={lane.name}` per program — the
information was there and nothing read it. Falls back to the queue set for a
deployment that names no node, and `docker-compose.yml` now sets one per
service so the multi-service stack gets it too.

The roster keys on the LANE's queue set when the node resolves, which is the
same string the row already had while it was consuming — so an existing row
keeps updating rather than a second one appearing.

Migration 0106 deletes the one key the bug produced, `celery:`. Deliberately
NOT a retention sweep: the roster never forgets on purpose, so a quiet row is
what it is FOR, and only a row that cannot correspond to anything real is safe
to remove. An `agent:agent` row, if one exists, is left alone — nothing here
can tell an abandoned agent id from a second agent that is genuinely down, and
hiding a dead GPU agent is the one thing the roster must not do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
2026-09-23 15:21:21 -04:00
co-authored by Claude Opus 5
parent a4c66601db
commit 48108a3569
8 changed files with 336 additions and 7 deletions
+101
View File
@@ -570,3 +570,104 @@ def test_the_sizing_task_is_registered_and_scheduled():
tasks = {e["task"] for e in entries.values()}
assert "backend.app.tasks.maintenance.reconcile_worker_lanes" not in tasks
assert "backend.app.tasks.maintenance.autoscale_worker_lanes" not in tasks
# --- a lane that is OFF is still its own lane ---------------------------------
#
# The operator's screen, 2026-09-23: "ML tagging has not checked in for 135
# min" beside a phantom row called "Worker ()" reported as running. One cause:
# a worker was attributed to its lane by the queues it was CONSUMING, and a
# lane at cap 0 has its consumers cancelled, so it consumes none and matched
# nothing.
def _stub_active_queues(monkeypatch, by_host):
"""Only `active_queues` — the read that decides which lane a node IS."""
class _Insp:
def __init__(self, **_):
pass
def active_queues(self):
return by_host
def stats(self):
return {h: {"pool": {"max-concurrency": 1}} for h in by_host}
def active(self):
return {}
def reserved(self):
return {}
control = _stub_control(monkeypatch)
control.inspect = _Insp
import sys
import types
mod = types.ModuleType("backend.app.celery_app")
class _C:
pass
c = _C()
c.control = control
mod.celery = c
monkeypatch.setitem(sys.modules, "backend.app.celery_app", mod)
def test_a_lane_with_its_consumers_cancelled_is_still_found(monkeypatch):
"""THE bug. `ml@host` consuming nothing must read as the ml lane, present,
rather than as no lane at all.
Attributed by the NODE NAME, which survives having no consumers —
`gen_supervisord` sets `CELERY_NODENAME={lane.name}` per program for
exactly this."""
_stub_active_queues(monkeypatch, {"ml@abc123": []})
live = wc.inspect_lanes_sync()
assert live["ml"].present is True
assert live["ml"].consuming == set(), "it is present AND consuming nothing"
def test_that_is_what_keeps_the_container_healthy(monkeypatch):
"""Why it mattered more than a cosmetic row.
`healthcheck._lanes_ok` requires every lane to be present, and ML ships at
cap 0 — so a fresh install reported a lane not answering, the container
went permanently unhealthy, and Swarm restarts an unhealthy task forever.
"""
_stub_active_queues(monkeypatch, {
"worker@a": [{"name": q} for q in LANES_BY_NAME["worker"].queues],
"scheduler@a": [{"name": q} for q in LANES_BY_NAME["scheduler"].queues],
"maintenance_long@a": [
{"name": q} for q in LANES_BY_NAME["maintenance_long"].queues
],
"ml@a": [], # off, as it ships
})
live = wc.inspect_lanes_sync()
missing = sorted(name for name, s in live.items() if not s.present)
assert missing == [], f"the healthcheck would fail the container: {missing}"
def test_a_node_this_build_did_not_name_still_matches_on_its_queues(monkeypatch):
"""The fallback. The multi-service compose stack ran every worker as
`celery@<host>`, and a deployment that sets no node name must keep
working — the node name is an improvement, not a requirement."""
_stub_active_queues(monkeypatch, {
"celery@xyz": [{"name": q} for q in LANES_BY_NAME["worker"].queues],
})
assert wc.inspect_lanes_sync()["worker"].present is True
def test_an_unknown_worker_is_still_ignored_rather_than_guessed_at(monkeypatch):
"""A deployment slicing CELERY_QUEUES differently has no lane row to
control. It belongs to the roster, not here — and must not be attributed
to whichever lane happens to be first."""
_stub_active_queues(monkeypatch, {"celery@xyz": [{"name": "something-else"}]})
live = wc.inspect_lanes_sync()
assert all(not s.present for s in live.values())