"""worker_lane — the most workers the operator will let each lane use. Milestone 422 step 1, reshaped 2026-09-23. One row per lane in `services/worker_lanes.LANES`, and ONE COLUMN an operator sets. ## Why there is only one number now There were three — `slots`, `slots_cap` and `autoscale` — because the manual dial was built first and the autoscaler arrived last, beside a control that already existed rather than in place of it. Operator: *"auto should be always on, not a setting, so that idle instances quiet down when not running. the number that is visible and something the user can tweak and manage should be the cap itself the number of running workers is handled by the autoscaling function which is always on."* So `slots` is gone. How many workers a lane is running right now is a MEASUREMENT — read live from the worker, moved by the autoscaler, never stored. Storing it made it look like a preference, which meant the operator had to keep two numbers in agreement and the autoscaler had to be told it was allowed to touch one of them. `autoscale` is gone for the same reason: it gated the mechanism behind a choice, and a lane nobody opted in simply never gave its slots back. `enabled` is gone too, and is now DERIVED: a cap of zero means no consumers. "Off" and "may use no workers" were two spellings of one fact, free to disagree. ## What remains 1 <= live pool <= slots_cap <= derived_ceiling (autoscaler) (this row) (computed) The floor is one PROCESS, not zero: billiard will not run an empty pool, and the parked process is what `add_consumer` lands on when the cap goes back up. The DERIVED CEILING is deliberately absent from this table. It is computed from the container's cgroup limits on every read, so a row written on a 32GB host and later run in a 4GB container is bounded by the 4GB — a stored ceiling would quietly authorise what the box can no longer hold. """ from datetime import datetime from sqlalchemy import ( CheckConstraint, DateTime, Integer, String, func, ) from sqlalchemy.orm import Mapped, mapped_column from .base import Base class WorkerLane(Base): __tablename__ = "worker_lane" __table_args__ = ( # Bare name — Base.metadata's naming convention prepends # ck_worker_lane_. Pre-prefixing here doubles it, which is what # alembic 0088 had to rename four constraints for (#3275). CheckConstraint("slots_cap >= 0", name="cap_non_negative"), ) # The lane name from services/worker_lanes.LANES — never a container # hostname. See models/service_seen.py for why: celery's worker names here # are `celery@`, minted fresh on every deploy. name: Mapped[str] = mapped_column(String(32), primary_key=True) # The most workers this lane may use. Zero means off — no consumers, so # the lane takes no work and (for ML) downloads no model. # # There is no upper CHECK here, because the bound it would need is the # derived ceiling, and no column holds that: it depends on the cgroup the # container is running in right now. Enforced at write instead. slots_cap: Mapped[int] = mapped_column(Integer, nullable=False) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(), )