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
+77
View File
@@ -52,3 +52,80 @@ def test_the_round_trip_count_matches_the_calls_actually_made():
f"_inspect_celery_sync makes {calls} inspect calls but "
f"INSPECT_ROUND_TRIPS says {sr.INSPECT_ROUND_TRIPS}"
)
# --- a lane that is off keeps its own row ------------------------------------
def _stub_inspect(monkeypatch, active_queues):
class _Insp:
def __init__(self, **_):
pass
def active_queues(self):
return active_queues
def active(self):
return {}
class _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_no_consumers_keeps_the_row_it_had_while_running(monkeypatch):
"""The phantom, and the reason the operator had two wrong rows at once.
A lane at cap 0 cancels its consumers, so it answers with an EMPTY queue
list. Grouped on that, it landed under the key `celery:` and rendered as a
row called `Worker ()` — reported running — while the real lane's row went
stale beside it because nothing updated it any more.
Keyed on the LANE's queue set now, which is the same string the row
already had while the lane was consuming. So turning a lane off updates
its row instead of minting a second one.
"""
from backend.app.services.worker_lanes import LANES_BY_NAME
_stub_inspect(monkeypatch, {"ml@abc123": []})
grouped = sr._inspect_celery_sync()
assert list(grouped) == [LANES_BY_NAME["ml"].queue_key]
assert () not in grouped, "the empty queue set is the phantom `Worker ()`"
def test_the_row_is_the_same_one_whether_the_lane_is_consuming_or_not(monkeypatch):
"""Stated as an identity rather than as two separate assertions: if these
keys ever differ, turning a lane off silently starts a second roster row
and the first goes stale — which is exactly what happened."""
ml_queues = [{"name": "ml"}]
_stub_inspect(monkeypatch, {"ml@abc123": ml_queues})
on = set(sr._inspect_celery_sync())
_stub_inspect(monkeypatch, {"ml@abc123": []})
off = set(sr._inspect_celery_sync())
assert on == off
def test_a_worker_this_build_did_not_name_is_still_grouped_by_its_queues(
monkeypatch,
):
"""The fallback, and the case the roster exists to report honestly: a
deployment slicing CELERY_QUEUES differently gets its raw queue list
rather than a name this code invented for it."""
_stub_inspect(monkeypatch, {"celery@xyz": [{"name": "odd"}]})
assert list(sr._inspect_celery_sync()) == [("odd",)]