feat: one image for every lane, with the model fetch gated on enabling (4296)
CI / lint (push) Successful in 2s
CI / extension-version (push) Successful in 2s
Build images / sign-extension (push) Successful in 3s
Build images / build-agent (push) Successful in 6s
CI / frontend-build (push) Successful in 20s
extension / lint (push) Successful in 23s
CI / backend-lint-and-test (push) Failing after 31s
CI / integration (push) Successful in 2m16s
Build images / build-ml (push) Successful in 3m8s
Build images / build-web (push) Successful in 3m16s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped

Milestone 422 step 6. Dockerfile.ml is gone; the main image carries torch,
torchvision, transformers, onnxruntime and opencv, and serves every lane.

WHY IT HAD TO MERGE: step 5 runs every lane in one process tree, so a second
image would mean the `ml` lane could never be enabled from the UI — there
would be no worker in that container to enable. The switch needs something to
switch.

THE MODEL NO LONGER DOWNLOADS AT BOOT. `entrypoint.sh`'s ml-worker role ran
download_models before celery started, so every boot of that role reached
HuggingFace for ~3.5GB — a startup dependency on a third party for a feature
the operator may never use. Rule 164 permits a runtime fetch only for
something "optional and clearly off", so the fetch is now a TASK, enqueued
the moment the lane is ENABLED.

Being a task is what makes it visible: it gets a TaskRun row, so the download
shows in Activity with a duration and a status, and a failure is something an
operator can see and retry rather than a container that quietly never became
useful. Idempotent, so re-enabling a provisioned lane costs one no-op.

Enqueued only when the lane actually came ON (`enabled is True`, not the
resolved value) so re-saving slots does not re-fetch, and only when the
consumer change landed — a task queued onto a queue nothing consumes would
sit pending with no explanation.

`fabledcurator-ml` KEEPS PUBLISHING, from the merged Dockerfile. The
operator's Swarm stack references that name and lives outside this repo;
dropping it would not break their deploy, it would freeze it silently at the
last publish — the exact failure class this milestone keeps finding. Retiring
the NAME is its own task, gated on that stack moving. Same two-phase shape
#406 used for pixiv.

THREE LIVE BREAKAGES from deleting the file, found by grepping for it rather
than assuming the build was the only consumer:

- `docker-compose.override.yml` built the ml service from it (contributor
  path would have failed at `docker compose build`).
- `tests/test_artifact_paths.py` pins the ml path set.
- `scripts/artifacts.sh` ML_PATHS named it. A path set naming a deleted file
  silently stops contributing to the derived revision — which the reuse check
  and the version string both read. That is #3202's recorded shape.

The `--with-ml` flag is gone from the generator and the healthcheck rather
than left defaulting to true. One image carries every lane now, so a flag
that can only be passed one way is a branch pretending to be a choice.

The advisory shipped in ecbd325 is what makes this honest to an adopter: the
lane says it is optional, names the model, and gives its download and
per-slot RAM before the switch is thrown.

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-22 08:51:44 -04:00
co-authored by Claude Opus 5
parent ecbd325437
commit ffcd13096a
15 changed files with 183 additions and 102 deletions
+16 -6
View File
@@ -50,13 +50,23 @@ case "$ROLE" in
;;
ml-worker)
echo "[entrypoint] Ensuring ML models present in /models..."
python -m backend.app.scripts.download_models
echo "[entrypoint] Starting ML Celery worker (ml queue)"
# NO MODEL DOWNLOAD HERE (milestone 422 step 6). This used to run
# download_models before celery started, which made every boot of this
# role reach HuggingFace for ~3.5GB. Rule 164 permits a runtime fetch only
# for a feature that is "optional and clearly off" — so the fetch moved to
# the moment the operator ENABLES the lane, where it is visible, retryable
# and attributable, instead of being a silent precondition of starting.
#
# The worker therefore starts with no model present, which is correct: it
# is not consuming the ml queue until the lane is enabled, and enabling it
# is what enqueues ensure_models.
QUEUES="${CELERY_QUEUES:-ml}"
CONCURRENCY="${CELERY_CONCURRENCY:-1}"
echo "[entrypoint] Starting ML Celery worker queues=$QUEUES concurrency=$CONCURRENCY"
exec celery -A backend.app.celery_app:celery worker \
--loglevel=info \
-Q ml \
--concurrency=1
-Q "$QUEUES" \
--concurrency="$CONCURRENCY"
;;
all)
@@ -75,7 +85,7 @@ case "$ROLE" in
# adjust a baseline that already works, and can never prevent a boot.
CONF="${SUPERVISOR_CONF:-/tmp/supervisord.conf}"
echo "[entrypoint] Generating $CONF from the lane table"
python -m backend.app.scripts.gen_supervisord ${FC_WITH_ML:+--with-ml} > "$CONF"
python -m backend.app.scripts.gen_supervisord > "$CONF"
echo "[entrypoint] Starting supervisord (web + worker lanes)"
exec supervisord -c "$CONF"
;;