CI and images / lint (push) Failing after 3s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 19s
CI and images / backend-lint-and-test (push) Failing after 30s
CI and images / integration (push) Failing after 2m15s
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
Run 7365, integration lane:
(psycopg.errors.UndefinedObject) constraint
"ck_worker_lane_ck_worker_lane_slots_within_cap" of relation
"worker_lane" does not exist
That is #3275 exactly, from the other direction. alembic 0088 had to RENAME
four constraints CREATED with a doubled prefix; this one tried to DROP two
with the same doubling. `op.drop_constraint` runs its name through
Base.metadata's naming convention, which prepends `ck_worker_lane_` to a
string that already carries it — `op.f()` is what marks a name as final, and
0103 used it on the way in.
The model test also went red, correctly: `test_worker_lane_check_constraints`
was parametrised over (slots, cap) pairs and asserted all three constraints,
and two of them went with the `slots` column. It is one unparametrised test
now, asserting the whole remaining set rather than a membership — a constraint
left behind naming a dropped column is not a harmless leftover, it is a table
the migration cannot have produced.
Worth recording: **the gate worked.** Run 7365 skipped `sign-extension`,
`build-web`, `smoke-web`, `promote` and `build-agent`, and `:dev` still names
the previous digest. That is the red-direction verification #4339 owed, and it
arrived by accident rather than by a forced failure — which is the better
evidence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
134 lines
5.5 KiB
Python
134 lines
5.5 KiB
Python
"""worker_lane — one number: the cap. `slots`, `enabled` and `autoscale` go.
|
|
|
|
Milestone 422, reshaped by the operator 2026-09-23:
|
|
|
|
"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."
|
|
|
|
## What each dropped column was, and why it is not needed
|
|
|
|
**`slots`** — how many workers the lane should run. That is a MEASUREMENT,
|
|
not a preference: the autoscaler moves the live pool between one and the cap
|
|
according to the backlog, and reads it back from the worker every minute.
|
|
Storing it made it look like something to keep in agreement with the cap,
|
|
which is exactly what the operator had to do.
|
|
|
|
**`autoscale`** — whether the lane was allowed to size itself. It gated the
|
|
mechanism behind a per-lane opt-in, so a lane nobody enabled simply never
|
|
gave its slots back. Always on now, which is the only way "idle instances
|
|
quiet down" can be true of an install nobody has configured.
|
|
|
|
**`enabled`** — whether the lane consumes its queues. Derived from `cap > 0`.
|
|
It and `slots = 0` were two spellings of one fact and were free to disagree;
|
|
this migration picks the one an operator can see.
|
|
|
|
## Why the caps are rewritten rather than preserved
|
|
|
|
The old defaults were 4 / 2 / 2 / 1, chosen when the number meant "the most
|
|
you may raise SLOTS to" — a bound on a manual control, deliberately loose
|
|
because moving within it was the ordinary act. The number now means "the most
|
|
workers this lane may actually use", which is a different promise, and
|
|
carrying the old figure over would silently quadruple the worker lane on
|
|
every existing install at the moment this deploys.
|
|
|
|
So every row is reset to the new defaults: **one for each required lane, zero
|
|
for ML.** That loses whatever an operator had set — which is the honest
|
|
trade, because what they set was an answer to a different question. The UI
|
|
now tells a busy lane's operator to raise its cap, which is how the number
|
|
gets back up on an install that needs it.
|
|
|
|
ML at zero also keeps rule 164's carve-out intact: no consumers, so no model
|
|
download until someone raises the cap.
|
|
|
|
Revision ID: 0105
|
|
Revises: 0104
|
|
Create Date: 2026-09-23
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
|
|
revision: str = "0105"
|
|
down_revision: Union[str, None] = "0104"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
# (name, cap) — the same values `services/worker_lanes.LANES` declares. Seeded
|
|
# here as literals rather than imported: a migration must describe the schema
|
|
# at ITS point in history, and importing the live table would make this file
|
|
# change meaning every time that table does.
|
|
_CAPS = (
|
|
("worker", 1),
|
|
("scheduler", 1),
|
|
("maintenance_long", 1),
|
|
("ml", 0),
|
|
)
|
|
|
|
|
|
def upgrade() -> None:
|
|
# The constraints go first: they name `slots`, so dropping the column out
|
|
# from under them fails on Postgres.
|
|
#
|
|
# `op.f()` around each name, and it is load-bearing. Without it alembic
|
|
# runs the name through Base.metadata's naming convention, which prepends
|
|
# `ck_worker_lane_` to a string that already carries it — and the DROP
|
|
# goes looking for `ck_worker_lane_ck_worker_lane_slots_within_cap`, which
|
|
# no database has. That is #3275 exactly, from the other direction:
|
|
# alembic 0088 had to RENAME four constraints created with the same
|
|
# doubling. Caught here by the integration lane, run 7365.
|
|
op.drop_constraint(
|
|
op.f("ck_worker_lane_slots_within_cap"), "worker_lane", type_="check",
|
|
)
|
|
op.drop_constraint(
|
|
op.f("ck_worker_lane_slots_non_negative"), "worker_lane", type_="check",
|
|
)
|
|
op.drop_column("worker_lane", "slots")
|
|
op.drop_column("worker_lane", "enabled")
|
|
op.drop_column("worker_lane", "autoscale")
|
|
|
|
# Reset to the new meaning. See the docstring: the old value answered a
|
|
# different question, and carrying it over would raise every lane.
|
|
for name, cap in _CAPS:
|
|
op.execute(
|
|
sa.text("UPDATE worker_lane SET slots_cap = :cap WHERE name = :name")
|
|
.bindparams(cap=cap, name=name)
|
|
)
|
|
|
|
# A lane the old seed never wrote — or one an operator added by hand — is
|
|
# left alone rather than guessed at. `_rows_by_name` creates any missing
|
|
# row at the lane's default on first read.
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.add_column(
|
|
"worker_lane",
|
|
sa.Column("slots", sa.Integer(), nullable=False, server_default="1"),
|
|
)
|
|
op.add_column(
|
|
"worker_lane",
|
|
sa.Column(
|
|
"enabled", sa.Boolean(), nullable=False, server_default=sa.text("true"),
|
|
),
|
|
)
|
|
op.add_column(
|
|
"worker_lane",
|
|
sa.Column(
|
|
"autoscale", sa.Boolean(), nullable=False, server_default=sa.text("false"),
|
|
),
|
|
)
|
|
# Restore the pre-0105 invariants. `slots` comes back as 1 everywhere and
|
|
# the caps are 1/1/1/0, so a lane at cap 0 would violate `slots <= cap` —
|
|
# hence the clamp before the constraint is added.
|
|
op.execute(sa.text("UPDATE worker_lane SET slots = 0 WHERE slots_cap = 0"))
|
|
op.execute(sa.text("UPDATE worker_lane SET enabled = (slots_cap > 0)"))
|
|
op.create_check_constraint(
|
|
op.f("ck_worker_lane_slots_non_negative"), "worker_lane", "slots >= 0",
|
|
)
|
|
op.create_check_constraint(
|
|
op.f("ck_worker_lane_slots_within_cap"), "worker_lane", "slots <= slots_cap",
|
|
)
|