18 KiB
work_item, stage, author_agent, status, created_at, model_used
| work_item | stage | author_agent | status | created_at | model_used |
|---|---|---|---|---|---|
| ORCH-062 | architecture | architect | proposed | 2026-06-09 | claude-opus-4-8 |
ADR-001: Авто-prune docker build cache — фоновый heartbeat-демон, выполняющий docker builder prune на хосте через ssh
Work Item: ORCH-062 — INFRA: авто-prune docker build cache на mva154
Стадия: architecture
Сквозная регистрация: docs/architecture/adr/adr-0025-build-cache-pruner.md (кросс-каттинг —
вводит новый фоновый компонент в ряду reconciler/job_reaper/disk_watchdog).
Статус
Proposed
Контекст
07.06.2026 хост-диск mva154 тихо дорос до 100% и положил весь self-hosting-конвейер всех
проектов (один прод-инстанс orchestrator на общей БД/очереди обслуживает и enduro-trails, и
orchestrator). Доминирующий «пожиратель» — docker build cache (≈11 ГБ), накопленный частыми
пересборками (docker compose up -d --build при прод-деплое; пересборка staging-образа
--profile staging; build-once retag за check_staging_image_fresh, ORCH-058). ORCH-063 ввёл
disk-watchdog, который только сигнализирует (Telegram-алерт ≥85%) и явно отложил авто-очистку в
отдельную задачу. ORCH-062 — эта задача.
BRD/ТЗ ставят развилку реализации (06-adr решает):
- A — heartbeat-демон в приложении (
src/build_cache_pruner.py), 1:1 наsrc/disk_watchdog.py. - B — host
daemon.json builder.gc.defaultKeepStorage(BuildKit GC, инфра-процедура Owner). - C — host-cron
docker builder prune -af --filter until=24h(инфра-процедура Owner).
Факты, сверенные с кодом (важно для выбора):
- Контейнер
orchestratorНЕ содержитdockerCLI.Dockerfile:11ставит толькоopenssh-client git curl ca-certificates.src/image_freshness.py::image_revisionпрямо фиксирует: «dockerlives on the HOST (the container ships onlyopenssh-client git), so whenssh_targetis given the inspect runs over ssh». → Любая docker-операция приложения над хостом идёт через ssh на хост (ssh deploy_ssh_user@deploy_ssh_host docker …), как уже делаютimage_freshnessиself_deploy(Phase B). Допущение BRD A-1 («docker.sock смонтирован → приложение может вызватьdocker builder prune») верно на уровне сокета, но не даёт готового CLI; raw-HTTP-over-UDS — лишний код против существующего ssh-канала. - В оркестраторе уже три проверенных фоновых daemon-потока с единым каркасом
(
threading.Thread(daemon=True)+threading.Event,start()/stop(timeout)/status(), per-tick never-raise, kill-switch, снимок вGET /queue):reconciler(ORCH-053),job_reaper(ORCH-065),disk_watchdog(ORCH-063,src/disk_watchdog.py). - ssh-канал на хост сконфигурирован и доступен:
settings.deploy_ssh_user(дефолтslin),settings.deploy_ssh_host; ключи проброшены ro (~/.orchestrator-ssh → /home/slin/.ssh, ORCH-040);slin— в группе docker (деплой-хук запускаетdocker composeна хосте). docker builder pruneпо контракту BuildKit затрагивает только build cache, не останавливает контейнеры и не удаляет образы запущенных сервисов (основа BR-3).
Решение
Сводка
Выбран Вариант A — фоновый heartbeat-демон src/build_cache_pruner.py, смоделированный
1:1 на src/disk_watchdog.py (тот же каркас, контракт, kill-switch, never-raise, блок в
GET /queue), который периодически выполняет docker builder prune на ХОСТЕ через ssh —
тем же каналом deploy_ssh_user@deploy_ssh_host, что уже используют image_freshness и
self_deploy. Это «вторая половина» disk-watchdog: watchdog сигналит — pruner убирает.
Варианты B и C отклонены (см. «Альтернативы»). Вариант C сохраняется как
задокументированный ручной fallback в 07-infra-requirements.md на случай, если ssh-канал
недоступен.
D1 — Механизм: фоновый демон приложения (A), не host-инфра (B/C) — BR-1/FR-1
Новый leaf-модуль src/build_cache_pruner.py (без обратных зависимостей на
stage_engine/stages/qg, как disk_watchdog/serial_gate/task_deps). Класс
BuildCachePruner с каркасом disk_watchdog: daemon-поток, чистый стоп через
_stop.wait(interval), контракт start()/stop(timeout)/status(), модульный singleton
build_cache_pruner. Каждые build_cache_prune_interval_s (дефолт 21600с = 6ч, NFR-4
«порядка часов») один тик выполняет уборку. Выбор A над B/C даёт: наблюдаемость в GET /queue,
kill-switch из конфига, golden-source-в-git, юнит-тесты, и симметрию с disk-watchdog (один
паттерн на два смежных эксплуатационных демона) — это снижает стоимость сопровождения и
когнитивную нагрузку следующего агента.
D2 — Команда и политика удержания: строго BuildKit GC с возрастным фильтром — BR-2/BR-3/FR-2/FR-3
- Команда уборки — строго
docker builder prune -f --filter until=<until>(BuildKit GC). Дефолтuntil=24h(build_cache_prune_until, ориентир из бизнес-запроса): удаляется build cache старше 24ч, свежий тёплый кэш недавних сборок сохраняется (BR-2/AC-2). - Флаг
-a/--all— только опционально (build_cache_prune_all, дефолтFalse) и всегда в паре с возрастным фильтром; «снести весь кэш» (prune -afбезuntil) запрещён дефолтом. - Жёстко запрещены
docker image prune,docker system prune, любое удаление образов запущенных сервисов, любая остановка/рестарт контейнеров. Затрагивается только build cache (BR-3/AC-3). Уборка никогда не рестартит/не роняет прод-контейнерorchestrator(групповой риск self-hosting).
D3 — Канал исполнения: ssh на хост (CLI в контейнере нет) — BR-3/FR-3/NFR-1
- Уборка исполняется на хосте:
ssh -o StrictHostKeyChecking=no <deploy_ssh_user@deploy_ssh_host> "docker builder prune -f --filter until=<until>", по образцуimage_freshness.image_revision(ssh_target-ветка). Это где физически живёт build cache (host docker daemon). - Нет ssh-таргета (
deploy_ssh_hostпуст) → тик no-op (лог +status()отражает причину). Это естественно скоупит фичу на self-hosting-прод (где ssh настроен) и делает дефолт безопасным для любого окружения без host-доступа — параллель тому, какself_deploy/image_freshnessдеградируют без_ssh_target(). - Вызов bounded таймаутом (
build_cache_prune_timeout_s, дефлот 120с) и неблокирующий конвейер (отдельный daemon-поток). Любой сбой — ниже D6.
D4 — Конфиг, kill-switch, дефолты — BR-5/BR-6/FR-5/NFR-3
Новый блок флагов в src/config.py рядом с disk_monitor_* (env-префикс ORCH_BUILD_CACHE_PRUNE_*):
Поле (settings.*) |
env | Дефолт | Назначение |
|---|---|---|---|
build_cache_prune_enabled |
ORCH_BUILD_CACHE_PRUNE_ENABLED |
True |
kill-switch; False → демон не стартует, поведение 1:1 как до задачи (NFR-3) |
build_cache_prune_interval_s |
ORCH_BUILD_CACHE_PRUNE_INTERVAL_S |
21600 (6ч) |
период тика, сек |
build_cache_prune_until |
ORCH_BUILD_CACHE_PRUNE_UNTIL |
24h |
возраст удержания (--filter until=) |
build_cache_prune_all |
ORCH_BUILD_CACHE_PRUNE_ALL |
False |
добавить -a (только в паре с until) |
build_cache_prune_timeout_s |
ORCH_BUILD_CACHE_PRUNE_TIMEOUT_S |
120 |
таймаут ssh-команды, сек |
build_cache_prune_notify_min_gb |
ORCH_BUILD_CACHE_PRUNE_NOTIFY_MIN_GB |
0 |
Telegram при освобождении ≥ N ГБ; 0 → тихо (без нотификаций) |
Дефолт enabled=True (обоснование, не самоочевидно): (а) бизнес-цель BR-1 — авто-предотвращение
инцидента без ручного вмешательства; дефолт False означал бы, что оператор обязан вспомнить и
включить флаг, что подрывает саму задачу; (б) операция документированно-безопасна (только build
cache, never images/containers/restart — D2/A-2); (в) при отсутствии ssh-таргета тик no-op (D3) →
фича безопасна-по-построению в любом окружении без host-доступа; (г) полностью обратима kill-switch.
Это сознательный, явно зафиксированный компромисс «безопасный дефолт vs авто-цель» в пользу
авто-цели, при сохранённой обратимости. Параллель: disk_monitor_enabled тоже дефолт True.
Валидаторы (паттерн _disk_positive_int/_disk_threshold_pct из config.py): невалидный
interval_s/timeout_s (не-int / ≤0) → лог-warning + дефолт; невалидный until (не матчит
^\d+[smhdw]?$) → лог-warning + 24h. Невалидное значение никогда не роняет старт (AC-6).
D5 — Наблюдаемость — BR-4/FR-4
Аддитивный read-only блок build_cache_prune в GET /queue (как disk_monitor):
enabled, interval_s, until, all, last_run_ts, last_reclaimed (распарсенное
Total reclaimed space: … из вывода docker builder prune, best-effort), last_error
(строка причины последнего сбоя/no-op, или null). status() — never-raise (минимум
{"enabled": …} при ошибке). Опционально — send_telegram при освобождении
≥ notify_min_gb (по образцу recovery-сообщения watchdog'а; дефолт выключено).
D6 — Инварианты и never-raise — NFR-1/NFR-2/NFR-5/FR-6, AC-4/AC-8
STAGE_TRANSITIONS, реестрQG_CHECKS,check_*,_parse_*,src/stage_engine.py, схема БД (src/db.py) — не изменяются. Pruner — эксплуатационный демон, не Quality Gate (категорияreconciler/job_reaper/disk_watchdog).- Без миграции БД: учёт «когда убирали в последний раз»/последний результат — in-memory, best-effort; сброс при рестарте безопасен (максимум одна лишняя безопасная уборка, NFR-5).
- never-raise на двух уровнях: per-команда (ненулевой rc / таймаут /
OSError/ недоступность ssh / parsing-ошибка вывода → лог + проглот, тик жив) и per-tick (внешнийtry/exceptв_run, какdisk_watchdog._run). Фоновый цикл и конвейер не падают. - Self-hosting: ssh выполняет
docker builder pruneна хосте подslin(в группе docker); команда не трогает образы/контейнеры запущенных сервисов; прод не рестартится. Обслуживаниеenduro-trailsв общем инстансе не затронуто.
D7 — Жизненный цикл (main.lifespan)
Старт демона — последним, сразу после disk_watchdog.start() (строки ~113–114 main.py);
стоп — первым в reverse-порядке, перед disk_watchdog.stop(). start() чтит kill-switch
(no-op при enabled=False), как DiskWatchdog.start().
Альтернативы
- Вариант B — host
daemon.json builder.gc.defaultKeepStorage— отвергнуто: применение конфигурации требует рестарта docker daemon на mva154, что останавливает ВСЕ контейнеры хоста (продorchestrator+ всё остальное) → катастрофический self-hosting blast radius (BRD C-1/R-3). Дополнительно: политика BuildKit GC — по объёму (defaultKeepStorage), а не по возрасту (BR-2 хочетuntil=24h); состояние не наблюдаемо вGET /queue(только хостовыйdocker system df); конфигурация — off-git host-артефакт. - Вариант C — host-cron
docker builder prune -af --filter until=24h— отвергнуто как основное (сохранено как ручной fallback в07): off-git невидимая инфра (следующий оператор/агент её не видит), нет наблюдаемости вGET /queue, нет kill-switch из конфига, не покрываетсяtests/— ломает принцип self-contained/reproducible/observable, которому следуют остальные демоны. - A через raw-HTTP по docker.sock (без ssh) — отвергнуто: требует ручного HTTP-over-UDS
клиента (chunked-ответы, версионирование API) — лишний код против уже существующего,
проверенного ssh-канала
image_freshness/self_deploy. - A через
dockerCLI, вкомпилированный в образ — отвергнуто: раздувает образ и требует пересборки/рестарта прода ради уборки; ssh-канал на хост уже есть и не трогает образ.
Последствия
- + Корень инцидента 07.06 (build cache → 100% диска) устраняется автоматически, без ручного вмешательства; тёплый кэш ≤24ч сохранён → штатные пересборки не «холодные».
- + Знакомый паттерн фонового демона (калька
disk_watchdog) → низкий риск, наблюдаемость вGET /queue, обратимость одним флагом, юнит-тестируемость, golden-source-в-git. - + Без новых внешних зависимостей и без рестарта docker daemon/прода (принцип «всё в Docker на одном сервере, минимум зависимостей»); ssh-канал переиспользован.
- − Зависимость от ssh-доступа на хост (как у
image_freshness/self_deploy); при отсутствии — тик no-op (наблюдаемо вstatus().last_error), фича просто не работает, но ничего не ломает. Митигейшн: документированный host-prerequisite + fallback-cron (07). - − In-memory учёт результата (без миграции) — допустим для эксплуатационного демона (не SLA).
- Откат:
ORCH_BUILD_CACHE_PRUNE_ENABLED=false→ демон не стартует, поведение 1:1 как до задачи; миграций БД нет, удалять нечего.
Ссылки
- BRD:
docs/work-items/ORCH-062/01-brd.md - TRZ:
docs/work-items/ORCH-062/02-trz.md - Acceptance:
docs/work-items/ORCH-062/03-acceptance-criteria.md - Инфра-требования:
docs/work-items/ORCH-062/07-infra-requirements.md - Тех-риски:
docs/work-items/ORCH-062/10-tech-risks.md - Сквозной ADR:
docs/architecture/adr/adr-0025-build-cache-pruner.md - Сверено по коду:
src/disk_watchdog.py(каркас-образец),src/image_freshness.py(image_revision/_ssh_target— ssh-канал к host docker),src/config.py(disk_monitor_*+ валидаторы,deploy_ssh_user/host),src/main.py(lifespanстарт/стоп демонов,GET /queue),Dockerfile:11(нет docker CLI в образе). - Родственные компоненты:
docs/architecture/adr/adr-0024-disk-watchdog.md(ORCH-063),adr-0007-reconciler.md,adr-0011-job-reaper-lease-reclaim.md.