[HIGH] tradein/deploy: список путей для пересборки скрапера — неполный allowlist, и PR #2675 уже уехал в контейнер, который его не исполняет #2679

Closed
opened 2026-08-05 20:36:28 +00:00 by bot-backend · 3 comments
Collaborator

Обнаружено при прод-верификации PR #2675. Деплой отчитался успехом, код в tradein-backend есть — а в tradein-scraper, который его и исполняет, нет.

Факт

tradein-backend   ghcr.io/...tradein-backend:latest   создан 23:07  → фикс есть (3 совпадения)
tradein-scraper   da26154c64a6                        создан 22:00  → фикса нет (0)

SCHEDULER_ENABLE:  scraper=true,  backend=false

То есть планировщик, который единственный запускает домовую оценку, работает на образе часовой давности. Правка не подействует вообще, пока контейнер не пересоздадут.

Причина: allowlist, а не denylist

В deploy-tradein.yml пересборка скрапера привязана к списку путей:

app/services/scrapers/**, app/services/scrape_pipeline.py,
app/services/scheduler.py, app/scheduler_main.py, app/tasks/**,
app/services/matching/**, app/services/house_dedup_merge.py,
packages/scraper-kit/**

PR #2675 трогает app/services/house_imv_backfill.py и app/services/product_handlers.pyни одного из них в списке нет, хотя оба исполняются в скрапере: первый содержит саму задачу, второй — финализацию статуса прогона.

Самое неприятное, что в этом же файле уже стоит комментарий о том, что так однажды случилось: «без этих путей scraper-контейнер оставался на старом коде (2026-07-02: fias-dedup доехал до tradein-backend, но не до tradein-scraper)». Тогда починили добавлением конкретных путей — то есть залатали случай, а не механизм. Через месяц тот же механизм выстрелил снова.

Почему это не разовая правка списка

Список перечисляет то, что вспомнили. Любой новый файл в app/services/, исполняемый планировщиком, по умолчанию попадает в дыру, и узнать об этом можно только прод-проверкой — которую делают не всегда.

Разумнее перевернуть логику: скрапер и бэкенд собираются из одного образа, значит пересобирать и пересоздавать надо оба, когда меняется что угодно из tradein-mvp/backend/** или packages/**. Экономия на пересоздании скрапера не стоит того, чтобы половина правок молча не доезжала.

Если пересоздавать всегда дорого — второй вариант: сравнивать хеш образа обоих контейнеров после деплоя и падать, если они разошлись. Тогда дыра станет видимой сразу, а не через месяц.

Что сделать с самим #2675

Срочности нет: 93.5% отказов домовой оценки — это отсутствие прокси (#2638), а не параметры. Правка доедет с ближайшим деплоем, который заденет любой из перечисленных путей, либо ручным запуском workflow. Но до этого момента считать её работающей нельзя.

Общий вывод

Это ровно тот же класс, что и весь сегодняшний список #2674: успех отчитывается там, где его никто не проверял. Деплой сказал «успех», код в одном контейнере есть — и этого хватило бы, если бы не привычка сверять эффект, а не факт деплоя.

Связано: #2675, #2674, #2462 (та же тема на стороне ПТИЦЫ), комментарий в deploy-tradein.yml от 2026-07-02.

Обнаружено при прод-верификации PR #2675. Деплой отчитался успехом, код в `tradein-backend` есть — **а в `tradein-scraper`, который его и исполняет, нет**. ## Факт ``` tradein-backend ghcr.io/...tradein-backend:latest создан 23:07 → фикс есть (3 совпадения) tradein-scraper da26154c64a6 создан 22:00 → фикса нет (0) SCHEDULER_ENABLE: scraper=true, backend=false ``` То есть планировщик, который единственный запускает домовую оценку, работает на образе часовой давности. **Правка не подействует вообще**, пока контейнер не пересоздадут. ## Причина: allowlist, а не denylist В `deploy-tradein.yml` пересборка скрапера привязана к списку путей: ``` app/services/scrapers/**, app/services/scrape_pipeline.py, app/services/scheduler.py, app/scheduler_main.py, app/tasks/**, app/services/matching/**, app/services/house_dedup_merge.py, packages/scraper-kit/** ``` PR #2675 трогает `app/services/house_imv_backfill.py` и `app/services/product_handlers.py` — **ни одного из них в списке нет**, хотя оба исполняются в скрапере: первый содержит саму задачу, второй — финализацию статуса прогона. Самое неприятное, что в этом же файле **уже стоит комментарий о том, что так однажды случилось**: «без этих путей scraper-контейнер оставался на старом коде (2026-07-02: fias-dedup доехал до tradein-backend, но не до tradein-scraper)». Тогда починили добавлением конкретных путей — то есть залатали случай, а не механизм. Через месяц тот же механизм выстрелил снова. ## Почему это не разовая правка списка Список перечисляет **то, что вспомнили**. Любой новый файл в `app/services/`, исполняемый планировщиком, по умолчанию попадает в дыру, и узнать об этом можно только прод-проверкой — которую делают не всегда. Разумнее перевернуть логику: скрапер и бэкенд собираются из **одного образа**, значит пересобирать и пересоздавать надо оба, когда меняется что угодно из `tradein-mvp/backend/**` или `packages/**`. Экономия на пересоздании скрапера не стоит того, чтобы половина правок молча не доезжала. Если пересоздавать всегда дорого — второй вариант: сравнивать хеш образа обоих контейнеров после деплоя и **падать**, если они разошлись. Тогда дыра станет видимой сразу, а не через месяц. ## Что сделать с самим #2675 Срочности нет: 93.5% отказов домовой оценки — это отсутствие прокси (#2638), а не параметры. Правка доедет с ближайшим деплоем, который заденет любой из перечисленных путей, либо ручным запуском workflow. Но **до этого момента считать её работающей нельзя**. ## Общий вывод Это ровно тот же класс, что и весь сегодняшний список #2674: **успех отчитывается там, где его никто не проверял**. Деплой сказал «успех», код в одном контейнере есть — и этого хватило бы, если бы не привычка сверять эффект, а не факт деплоя. Связано: #2675, #2674, #2462 (та же тема на стороне ПТИЦЫ), комментарий в `deploy-tradein.yml` от 2026-07-02.
Author
Collaborator

Working on this in PR #2680

Working on this in PR #2680
Author
Collaborator

PR #2680 готов, CI зелёный. Мержить не буду — правки деплой-конвейера по правилам репозитория идут через тебя. Ниже то, что нужно для решения.

Что меняется

Пересоздание скрапера больше не привязано к списку файлов. Условие теперь буквально повторяет условие сборки образа: «мог ли появиться новый образ». Список удалён целиком, поддерживать и забывать нечего.

Плюс сверка образов после деплоя: если бэкенд и скрапер разошлись, деплой падает с именем отставшего контейнера, а не заканчивается тихим успехом.

Плюс, по моей просьбе, пропуск ожидания, когда пересоздавать нечего: перед пятиминутным сливом прогонов сравнивается подтянутый образ с работающим. Совпали — идём дальше без ожидания.

Чем обошлось по времени

деплой было стало
backend-правка, образ реально новый +37с медиана, +72с среднее, до +5 мин в худшем
правка compose/workflow при том же образе не платит ничего
frontend-only не менялось не менялось

Платят только те деплои, которые раньше молча не доезжали до планировщика — те самые 48%.

Побочно поймали баг в первой версии этой же правки

Пока добавляли пропуск ожидания, выяснилось, что пометка осиротевших прогонов была завязана на «собирались пересоздавать», а не на «пересоздали». При правке конфигурации с тем же образом контейнер не пересоздаётся — а живой прогон всё равно получил бы статус «отменён» и продолжил бы работать отменённым.

Это ровно тот класс, что мы разбираем в #2674: статус, который врёт. Показательно, что нашлось не тестом, а вопросом «а что если платить не за что».

Известный потолок, оставлен сознательно

Правка compose, меняющая только конфигурацию сервиса (переменные, лимиты) при том же образе, пересоздаст контейнер без ожидания слива — сравниваются образы, а не конфигурация. Страхуют мягкая остановка, окно в две минуты и суточный сборщик зависших прогонов. Вариант «спросить у compose, будет ли пересоздание» отвергнут: разбор его вывода хрупче цены этого случая.

Что произойдёт при мерже

PR трогает сам конвейер, поэтому образ пересоберётся, сравнение увидит, что скрапер отстал (он и правда отстал прямо сейчас), и код PR #2675 наконец доедет до планировщика. Сверка после этого пройдёт зелёной.

Если бы расхождение почему-то осталось — деплой упал бы на новой сверке, и это самолечится: маркер не записывается, следующий прогон возьмёт ту же базу и пересоберёт накопленное.

PR #2680 готов, CI зелёный. **Мержить не буду — правки деплой-конвейера по правилам репозитория идут через тебя.** Ниже то, что нужно для решения. ## Что меняется Пересоздание скрапера больше не привязано к списку файлов. Условие теперь буквально повторяет условие сборки образа: «мог ли появиться новый образ». Список удалён целиком, поддерживать и забывать нечего. Плюс **сверка образов после деплоя**: если бэкенд и скрапер разошлись, деплой падает с именем отставшего контейнера, а не заканчивается тихим успехом. Плюс, по моей просьбе, **пропуск ожидания, когда пересоздавать нечего**: перед пятиминутным сливом прогонов сравнивается подтянутый образ с работающим. Совпали — идём дальше без ожидания. ## Чем обошлось по времени | деплой | было | стало | |---|---|---| | backend-правка, образ реально новый | — | **+37с медиана, +72с среднее**, до +5 мин в худшем | | правка compose/workflow при том же образе | — | **не платит ничего** | | frontend-only | не менялось | не менялось | Платят только те деплои, которые раньше **молча не доезжали** до планировщика — те самые 48%. ## Побочно поймали баг в первой версии этой же правки Пока добавляли пропуск ожидания, выяснилось, что пометка осиротевших прогонов была завязана на «собирались пересоздавать», а не на «пересоздали». При правке конфигурации с тем же образом контейнер не пересоздаётся — а живой прогон всё равно получил бы статус «отменён» и **продолжил бы работать отменённым**. Это ровно тот класс, что мы разбираем в #2674: статус, который врёт. Показательно, что нашлось не тестом, а вопросом «а что если платить не за что». ## Известный потолок, оставлен сознательно Правка compose, меняющая только конфигурацию сервиса (переменные, лимиты) при том же образе, пересоздаст контейнер **без** ожидания слива — сравниваются образы, а не конфигурация. Страхуют мягкая остановка, окно в две минуты и суточный сборщик зависших прогонов. Вариант «спросить у compose, будет ли пересоздание» отвергнут: разбор его вывода хрупче цены этого случая. ## Что произойдёт при мерже PR трогает сам конвейер, поэтому образ пересоберётся, сравнение увидит, что скрапер отстал (он и правда отстал прямо сейчас), и **код PR #2675 наконец доедет до планировщика**. Сверка после этого пройдёт зелёной. Если бы расхождение почему-то осталось — деплой упал бы на новой сверке, и это самолечится: маркер не записывается, следующий прогон возьмёт ту же базу и пересоберёт накопленное.
Author
Collaborator

ЗАКРЫВАЮ: механизм перевёрнут, дыра закрыта, на проде все три контейнера на одном образе

Проверено на живом проде 2026-08-07, а не по факту мержа.

tradein-backend   sha256:0121fc83dc91734ea0ffea06d5ab5c7ce66915902ba31da0484362f54c2bd1e1  08:50:51.613 UTC
tradein-scraper   sha256:0121fc83dc91734ea0ffea06d5ab5c7ce66915902ba31da0484362f54c2bd1e1  08:50:51.558 UTC
tradein-tgbot     sha256:0121fc83dc91734ea0ffea06d5ab5c7ce66915902ba31da0484362f54c2bd1e1  08:50:51.556 UTC

Один образ, три контейнера, разница создания 57 мс — то есть пересозданы одной командой,
а не по отдельности.

Механизм, а не заплатка. В deploy-tradein.yml:

  • allowlist путей для скрапера удалён целиком (строки 118-128 — вместо списка стоит объяснение,
    почему списка больше нет);
  • условие пересоздания теперь повторяет условие сборки образа: «мог ли появиться новый образ»
    (строки 311-330, 528-545);
  • после деплоя стоит сверка (строки 754-778): если tradein-scraper или tradein-tgbot бежит
    не тот образ, что tradein-backend, — exit 1 с именем отставшего контейнера и командой
    ручного лечения. «Контейнера нет» и «контейнер отстал» разведены как разные аварии.

Исходная жертва доехала. Правка #2675, из-за которой задача и заведена, теперь в живом
tradein-scraper: маркер normalize_house_type присутствует в задеплоенном
app/services/house_imv_backfill.py. Ровно то, чего не было 05.08.

Критерий выполнен числом. Закрываю.

## ЗАКРЫВАЮ: механизм перевёрнут, дыра закрыта, на проде все три контейнера на одном образе **Проверено на живом проде 2026-08-07, а не по факту мержа.** ``` tradein-backend sha256:0121fc83dc91734ea0ffea06d5ab5c7ce66915902ba31da0484362f54c2bd1e1 08:50:51.613 UTC tradein-scraper sha256:0121fc83dc91734ea0ffea06d5ab5c7ce66915902ba31da0484362f54c2bd1e1 08:50:51.558 UTC tradein-tgbot sha256:0121fc83dc91734ea0ffea06d5ab5c7ce66915902ba31da0484362f54c2bd1e1 08:50:51.556 UTC ``` Один образ, три контейнера, разница создания **57 мс** — то есть пересозданы одной командой, а не по отдельности. **Механизм, а не заплатка.** В `deploy-tradein.yml`: - allowlist путей для скрапера удалён целиком (строки 118-128 — вместо списка стоит объяснение, почему списка больше нет); - условие пересоздания теперь повторяет условие сборки образа: «мог ли появиться новый образ» (строки 311-330, 528-545); - после деплоя стоит сверка (строки 754-778): если `tradein-scraper` или `tradein-tgbot` бежит не тот образ, что `tradein-backend`, — `exit 1` с именем отставшего контейнера и командой ручного лечения. «Контейнера нет» и «контейнер отстал» разведены как разные аварии. **Исходная жертва доехала.** Правка #2675, из-за которой задача и заведена, теперь в живом `tradein-scraper`: маркер `normalize_house_type` присутствует в задеплоенном `app/services/house_imv_backfill.py`. Ровно то, чего не было 05.08. Критерий выполнен числом. Закрываю.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2679
No description provided.