Files
FabledCurator/tests/test_worker_lanes.py
T
bvandeusenandClaude Opus 5 45bb7044f7
CI and images / lint (push) Failing after 3s
CI and images / extension-version (push) Successful in 4s
CI and images / frontend-build (push) Successful in 31s
CI and images / backend-lint-and-test (push) Successful in 35s
CI and images / integration (push) Successful in 2m44s
CI and images / sign-extension (push) Skipped
CI and images / build-web (push) Skipped
CI and images / smoke-web (push) Skipped
CI and images / promote (push) Skipped
CI and images / build-agent (push) Skipped
feat: the System tab reads a stored sample instead of inspecting per load (4295)
Operator: "there is a repull every time this page loads is there a reason
this info isn't being tracked in the background and stored in some way?"

There was a reason and it had expired, and underneath it there was plain
waste.

The expired one: /api/system/workers was deliberately uncached because an
operator dragging the stepper must not be shown a pre-change value. That
stopped being true at 1353d34, when the UI began patching its row from the
write's reply instead of refetching.

The waste: size_worker_lanes already inspected the broker on a timer to
decide pool sizes — computing the pool, active, reserved and queue depth
the page shows, using them, and discarding them. The browser then asked
the broker for the same numbers four times a minute, per open tab.

So one inspect now feeds three things: the sizing decision, a stored
sample (worker_lane_sample, alembic 0107), and the celery roster. No
request path touches the broker at all — the roster refresh comes off
/api/system/health too, where it had been rate-limited to 20s and so made
worker liveness a function of whether anyone had a browser open.

Consequences, stated rather than hidden:

- The live figures are up to one sweep old. measured_at travels with each
  lane and the page says how old, because a stale number presented as
  current is how someone watches a queue "not move" that is moving.
- The sweep is the roster's only writer now, so its period and the
  staleness thresholds are in a relationship. 60s against a 90s stale
  threshold left one missed tick between normal and all-yellow — the
  shape of lesson #4355 — so the period is 30s, named once in
  worker_lanes, and system_health asserts its headroom at import with a
  test stating the same thing in prose.
- An idle lane therefore also gives a worker back twice as fast. That is
  the direction asked for: "idle instances quiet down when not running".

Also bounds the inspect in push_lane_cap, which was an await with no
deadline (rule 156) — harmless while it ran on a request, less so now
that it runs in a background task where a hang would be silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
2026-09-23 18:52:08 -04:00

403 lines
17 KiB
Python

"""Worker-lane definitions and the derived ceiling (milestone 422 step 1).
Pure — no DB, no broker. The ceiling reads files under /sys/fs/cgroup, so the
tests point it at temporary files instead of at whatever the CI container
happens to have.
"""
from __future__ import annotations
import pytest
from backend.app.services import worker_lanes as wl
# --- the lane definitions ----------------------------------------------------
def test_lane_names_are_unique_and_stable():
names = [lane.name for lane in wl.LANES]
assert len(names) == len(set(names))
# The key every settings row and every API call uses. Renaming one orphans
# its row silently, so the set is pinned rather than merely counted.
assert set(names) == {"worker", "scheduler", "maintenance_long", "ml"}
def test_queue_sets_are_unique():
"""Two lanes sharing a queue set would be indistinguishable to
`celery inspect`, which groups by exactly that (see service_roster)."""
keys = [lane.queue_key for lane in wl.LANES]
assert len(keys) == len(set(keys))
def test_every_routed_queue_has_a_lane_that_serves_it():
"""The check that actually matters: a queue celery routes tasks to, with
no lane consuming it, is work that queues forever and never runs.
Read from `celery_app`'s real routing table rather than a list written
here, so adding a route without a lane fails this test.
"""
from backend.app.celery_app import celery as celery_app
routes = celery_app.conf.task_routes or {}
routed = {spec["queue"] for spec in routes.values() if "queue" in spec}
served = {q for lane in wl.LANES for q in lane.queues}
assert routed - served == set(), (
f"queues with no lane to consume them: {sorted(routed - served)}"
)
def test_the_shipped_caps_are_one_of_each_with_ml_at_zero():
"""Operator, 2026-09-22: *"that starting value should be one of each."*
Restated 2026-09-23 when the cap became the ONLY number: *"the cap
defaults should be 1 and 0 for the ml-worker."*
ML at zero is milestone 422 step 6's requirement: a cap of zero means no
consumers, and raising it is what triggers the SigLIP download. Rule 164
allows a runtime fetch only for a feature that is optional and clearly
off, so a default of 1 here would make every fresh install reach
HuggingFace.
"""
by_name = wl.LANES_BY_NAME
for name in ("worker", "scheduler", "maintenance_long"):
assert by_name[name].default_slots_cap == 1
assert by_name["ml"].default_slots_cap == 0
def test_a_lane_has_exactly_one_operator_setting():
"""The whole point of the 2026-09-23 reshape. `slots`, `enabled` and
`autoscale` are gone: how many workers run is a measurement the sizing
pass owns, and "off" is a cap of zero.
Asserted against the dataclass's own fields rather than by name, so a
second knob added later fails here instead of quietly reappearing in the
UI beside the cap — which is exactly how three settings accumulated the
first time."""
import dataclasses
settable = {
f.name for f in dataclasses.fields(wl.Lane) if f.name.startswith("default_")
}
assert settable == {"default_slots_cap"}, (
f"a lane has more than one operator default: {sorted(settable)}"
)
def test_the_shipped_cap_is_never_the_ceiling_on_a_real_machine():
"""A cap that starts at the ceiling is a rubber stamp: there is nowhere to
raise it to, so the nudge that tells an operator to raise it would have
nothing to say. One-of-each leaves room on anything bigger than a
single-core box."""
for lane in wl.LANES:
assert lane.default_slots_cap >= 0
assert lane.default_slots_cap <= 1
def test_only_ml_is_memory_bound():
"""Which lane is memory-bound decides which ceiling formula applies. If
another lane ever becomes so, it needs its own per-slot cost — the ML
figure is about a SigLIP copy and means nothing for a thumbnailer."""
assert [lane.name for lane in wl.LANES if lane.memory_bound] == ["ml"]
# --- reading the container's limits ------------------------------------------
def _point_memory_at(monkeypatch, tmp_path, contents: str | None, *, v2=True):
target = tmp_path / "memory.max"
if contents is not None:
target.write_text(contents)
attr = "_CGROUP_V2_MEMORY" if v2 else "_CGROUP_V1_MEMORY"
monkeypatch.setattr(wl, attr, target)
# Neutralise the other tier so the test controls exactly one source.
other = "_CGROUP_V1_MEMORY" if v2 else "_CGROUP_V2_MEMORY"
monkeypatch.setattr(wl, other, tmp_path / "absent")
def test_memory_read_from_cgroup_v2(monkeypatch, tmp_path):
_point_memory_at(monkeypatch, tmp_path, str(8 * wl.GIB))
assert wl.container_memory_bytes() == 8 * wl.GIB
def test_memory_read_from_cgroup_v1(monkeypatch, tmp_path):
target = tmp_path / "limit_in_bytes"
target.write_text(str(6 * wl.GIB))
monkeypatch.setattr(wl, "_CGROUP_V2_MEMORY", tmp_path / "absent")
monkeypatch.setattr(wl, "_CGROUP_V1_MEMORY", target)
assert wl.container_memory_bytes() == 6 * wl.GIB
def test_v2_max_means_no_limit_not_zero(monkeypatch, tmp_path):
"""'max' is a container with no memory limit set. Falling through to host
RAM is right; reading it as 0 or as an error would cap a large machine at
the unknown-ceiling."""
_point_memory_at(monkeypatch, tmp_path, "max")
value = wl.container_memory_bytes()
assert value is not None and value > wl.GIB
def test_v1_sentinel_is_recognised_as_unlimited(monkeypatch, tmp_path):
"""cgroup v1 spells 'no limit' as a PAGE_SIZE-aligned LONG_MAX rather than
a word, so it is recognised by magnitude. Taken literally it would be
petabytes and the ML ceiling would be nonsense."""
target = tmp_path / "limit_in_bytes"
target.write_text(str(9223372036854771712))
monkeypatch.setattr(wl, "_CGROUP_V2_MEMORY", tmp_path / "absent")
monkeypatch.setattr(wl, "_CGROUP_V1_MEMORY", target)
value = wl.container_memory_bytes()
assert value is not None
assert value < wl._UNLIMITED_ABOVE
# --- the derived ceiling -----------------------------------------------------
def test_ml_ceiling_is_memory_divided_by_per_slot_cost(monkeypatch, tmp_path):
# 2 GiB reserved for web and the other lanes, then 4 GiB per model copy.
# Cores pinned high so the memory bound is the one being read here.
_point_memory_at(monkeypatch, tmp_path, str(14 * wl.GIB))
monkeypatch.setattr(wl, "container_cpu_count", lambda: 32)
assert wl.derived_ceiling(wl.LANES_BY_NAME["ml"]) == 3
def test_ml_is_bounded_by_cores_as_well_as_memory(monkeypatch, tmp_path):
"""The bug of 2026-09-23, in one assertion.
The ML ceiling was memory ALONE. On the operator's large-memory host that
offered ~49 slots; they took them, and each slot asks torch for
`threads_per_slot` cores — so the lane ran ~200 threads over a box that
has nowhere near that many. Embeds that should be seconds took 107-246s,
and the daily CCIP sweep sharing that pool died on its 1800s soft limit.
Memory here says 49. The cores say 8 / 4 = 2, and the smaller bound is the
only honest one: a control must not offer a number the machine cannot
feed.
"""
_point_memory_at(monkeypatch, tmp_path, str(200 * wl.GIB))
monkeypatch.setattr(wl, "container_cpu_count", lambda: 8)
ml = wl.LANES_BY_NAME["ml"]
assert ml.threads_per_slot == 4
assert wl.derived_ceiling(ml) == 2
def test_a_few_cores_still_offer_one_ml_slot_rather_than_none(
monkeypatch, tmp_path,
):
"""The two bounds fail differently, deliberately. Too little MEMORY is
honestly zero — the first task would OOM the container. Too few CORES is
merely slow, which is a trade an operator may want, so it floors at one
rather than making the lane unreachable on a small box."""
_point_memory_at(monkeypatch, tmp_path, str(200 * wl.GIB))
monkeypatch.setattr(wl, "container_cpu_count", lambda: 1)
assert wl.derived_ceiling(wl.LANES_BY_NAME["ml"]) == 1
def test_the_embedder_asks_for_exactly_what_the_ceiling_budgeted(monkeypatch):
"""The two halves of the same number, tied.
`threads_per_slot` is only meaningful because `embedder.load()` calls
`torch.set_num_threads` with it. It lived in the embedder as a private 4
beside a comment saying "keep N_replicas x this within the cores allotted
to ML" — a constraint stated where nothing could enforce it, and nothing
did. If these two ever drift, the ceiling is budgeting cores for a demand
the worker does not make, and nothing else would notice.
"""
from backend.app.services.ml import embedder
assert embedder._INTRA_OP_THREADS == wl.LANES_BY_NAME["ml"].threads_per_slot
def test_a_small_box_is_told_it_cannot_run_tagging(monkeypatch, tmp_path):
"""Honestly zero rather than a floor of one. A 4GB box cannot hold a model
alongside the web process, and offering a slot that OOMs the container the
first time it is used is precisely what the ceiling exists to prevent —
the operator's 'I don't want it to kill their servers'."""
_point_memory_at(monkeypatch, tmp_path, str(4 * wl.GIB))
assert wl.derived_ceiling(wl.LANES_BY_NAME["ml"]) == 0
def test_unreadable_memory_limit_yields_a_low_ceiling_not_an_unlimited_one(
monkeypatch, tmp_path
):
"""The failure DIRECTION is the point. Not knowing how much memory there
is must never read as 'plenty' — an unswept absence is not a verdict."""
_point_memory_at(monkeypatch, tmp_path, None)
monkeypatch.setattr(wl.os, "sysconf", lambda _: (_ for _ in ()).throw(OSError))
assert wl.derived_ceiling(wl.LANES_BY_NAME["ml"]) == wl.UNKNOWN_CEILING
def test_cpu_lanes_are_bounded_by_cores_not_memory(monkeypatch, tmp_path):
"""A CPU-bound lane on a memory-starved box must still get slots — its
slots are processes, not model copies."""
_point_memory_at(monkeypatch, tmp_path, str(2 * wl.GIB))
monkeypatch.setattr(wl, "container_cpu_count", lambda: 8)
assert wl.derived_ceiling(wl.LANES_BY_NAME["worker"]) == 8
def test_cpu_quota_beats_host_core_count(monkeypatch, tmp_path):
"""`os.cpu_count()` reports the HOST's cores from inside a container, so a
4-core quota on a 32-core host would otherwise offer 32 slots. The
operator's own stack sets `cpus: '4.0'` on ml-worker, so this is real
configuration rather than a hypothetical."""
quota = tmp_path / "cpu.max"
quota.write_text("400000 100000")
monkeypatch.setattr(wl, "_CGROUP_V2_CPU", quota)
monkeypatch.setattr(wl.os, "cpu_count", lambda: 32)
assert wl.container_cpu_count() == 4
def test_a_single_core_box_still_gets_a_slot(monkeypatch):
monkeypatch.setattr(wl, "container_cpu_count", lambda: 1)
assert wl.derived_ceiling(wl.LANES_BY_NAME["worker"]) >= wl.MIN_CEILING
def test_the_ceiling_is_computed_not_stored(monkeypatch, tmp_path):
"""The property the design rests on: the same lane yields a different
ceiling when the container's limits change, with no row edit. A stored
ceiling would keep authorising what the box no longer has."""
lane = wl.LANES_BY_NAME["ml"]
monkeypatch.setattr(wl, "container_cpu_count", lambda: 32)
_point_memory_at(monkeypatch, tmp_path, str(34 * wl.GIB))
big = wl.derived_ceiling(lane)
(tmp_path / "memory.max").write_text(str(10 * wl.GIB))
small = wl.derived_ceiling(lane)
assert big > small
def test_ceilings_covers_every_lane():
assert set(wl.ceilings()) == {lane.name for lane in wl.LANES}
# --- the duplicate this step collapsed ---------------------------------------
def test_role_names_is_derived_from_the_lane_table():
"""`service_roster.ROLE_NAMES` was a hand-kept second copy of 'queue set
-> display name' and had already drifted: maintenance_long is a live lane
with four task routes and a dedicated worker, and the roster did not know
its name, so the System tab rendered `Worker (maintenance_long)`.
Asserting the derivation rather than the contents — a test listing the
names again would be a third copy.
"""
from backend.app.services.service_roster import ROLE_NAMES, role_display_name
assert ROLE_NAMES == {lane.queue_key: lane.display_name for lane in wl.LANES}
assert role_display_name(("maintenance_long",)) == "Long maintenance"
def test_an_unrecognised_queue_set_still_gets_a_true_label():
"""A deployment slicing CELERY_QUEUES differently must not be handed a
name this code invented for it."""
from backend.app.services.service_roster import role_display_name
assert role_display_name(("nonsense",)) == "Worker (nonsense)"
# --- the model's invariant ---------------------------------------------------
def test_the_row_carries_exactly_one_constraint_now():
"""`slots >= 0` and `slots <= slots_cap` went with the `slots` column on
2026-09-23 — there is one number left, and the only thing that can be
wrong with it is being negative.
Asserted as the WHOLE set rather than as a membership check: a constraint
left behind naming a dropped column is not a harmless leftover, it is a
table the migration cannot have produced, and the model would then
describe a schema no database has.
"""
from backend.app.models import WorkerLane
constraints = {
c.name: str(c.sqltext) for c in WorkerLane.__table__.constraints
if hasattr(c, "sqltext")
}
# The names carry the convention's `ck_worker_lane_` prefix ALREADY — the
# model declares them bare and Base.metadata's naming_convention applies
# it. Asserting the prefixed form is what pins the thing that actually
# went wrong once: alembic 0088 had to rename four constraints that
# shipped as `ck_x_ck_x_name`, because the migration pre-prefixed a name
# the convention then prefixed again (#3275). A bare-name assertion here
# would pass just as happily against a doubled one.
assert constraints == {"ck_worker_lane_cap_non_negative": "slots_cap >= 0"}
for name in constraints:
assert not name.startswith("ck_worker_lane_ck_"), f"doubled prefix: {name}"
# --- what an optional lane tells the operator before it is enabled -----------
def test_the_ml_lane_is_marked_optional_and_declares_its_model():
"""The operator's ask, 2026-09-22: an optional lane must SAY it is
optional, and say what enabling it downloads — which model, how big, and
what it costs to hold — before the switch is thrown rather than after a
multi-GB fetch has begun."""
ml = wl.LANES_BY_NAME["ml"]
assert ml.optional is True
assert len(ml.models) == 1
assert ml.models[0].repo == "google/siglip-so400m-patch14-384"
assert ml.models[0].approx_download_bytes > 0
assert ml.models[0].approx_resident_bytes > 0
def test_no_required_lane_claims_a_model():
"""A lane the product cannot work without must not be gated behind a
download. If one ever needs a model, it stops being required."""
for lane in wl.LANES:
if lane.models:
assert lane.optional, f"{lane.name} needs a model but is not optional"
def test_the_per_slot_ceiling_is_the_same_number_the_ui_shows():
"""DERIVED, not restated. The figure quoted to the operator before they
enable the lane and the figure the cap enforces have to be one number, or
the UI promises a slot the cap will then refuse."""
ml = wl.LANES_BY_NAME["ml"]
assert wl.ML_BYTES_PER_SLOT == ml.models[0].approx_resident_bytes
def test_estimated_numbers_are_flagged_as_estimates():
"""`measured` travels with the figures so the card can say "about". An
estimate presented as a measurement is what decides whether someone's
server survives — it must not be rounded into something that reads like a
fact. Flip this to True in the same commit that records a real
measurement."""
assert wl.SIGLIP_MODEL.measured is False
# --- the sweep's cadence, against the thresholds that read it ----------------
def test_the_sweep_runs_often_enough_to_keep_the_roster_fresh():
"""The comparison that was never made for the GPU agent.
Its idle lease poll backed off to a 900s ceiling while the roster called it
stopped at 300s. Both numbers were right on their own, in different files,
written ten weeks apart — and an idle agent was structurally guaranteed to
read as stopped (lesson #4355).
`size_worker_lanes` is now the ONLY writer of the celery roster, so its
period and the staleness thresholds are in exactly that relationship. Two
clear sweeps before a part is even doubted: one missed tick is routine,
because the sweep rides the maintenance queue and does an inspect that can
take eleven seconds.
"""
from backend.app.api.system_health import (
DOWN_AFTER_SECONDS,
STALE_AFTER_SECONDS,
)
assert wl.SWEEP_PERIOD_SECONDS * 2 <= STALE_AFTER_SECONDS
assert wl.SWEEP_PERIOD_SECONDS * 2 <= DOWN_AFTER_SECONDS
def test_the_beat_schedule_is_the_same_number_and_not_a_copy_of_it():
"""A schedule that merely happens to equal the constant is one edit away
from disagreeing with the test above, which would then be asserting
headroom the running system does not have."""
from backend.app.celery_app import celery
entry = celery.conf.beat_schedule["size-worker-lanes"]
assert entry["schedule"] == wl.SWEEP_PERIOD_SECONDS