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
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:
@@ -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",)]
|
||||
|
||||
Reference in New Issue
Block a user