chore(tradein): разбор мёртвого кода — подключить, удалить или задокументировать (#2674) #2689
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2689
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "chore/2674-dead-code-sweep"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Восемь находок раздела «Мёртвый код» разобраны по одному вопросу: механизм рабочий и его некому позвать, или он невыразим/дублирует существующее, или это осознанный задел. Все числа сняты с прод-БД
tradein2026-08-06.loaded_at(12.07) — ровно один ручной запуск, 24 дня без обновления. Отсюда кормятсяhouses.year_built/material_walls/total_floors→listings.year_built, когортный фильтр эстиматораfilters_hashestimation.sale.data.filtersHash, Циан кладёт на уровень выше —estimation.sale.filtersHash. Колонка 0/1658, при том что в сохранённых сырых ответах хеш есть у 139/139 и все значения различныhas_panoramaHOUSE_FIELD_PRIORITY, обещан публичным контрактомmarket.v_houses— и не попадал вhousesни одной строкой кода (0 из 9366)_phys_dedup_key+_extract_street_token: 25 ссылок, все из тестов. Обёртки над живыми_lot_dedup_components/_parse_street_houseasking_to_sold_ratios_tiered(21 строка) +asking_to_sold_tier_bounds(5): ноль читателей, ноль писателей, флагаtier_aware_ratio_enabledне существует.computed_at27.06 — при том что живаяasking_to_sold_ratiosобновилась 05.08listings.merged_into(93 408 NULL, 0 упоминаний в коде; миграция 113 уже называла её мёртвой) иhouse_sources.raw_payload(49 502 NULL) вместе с GIN-индексом по всегда-NULL колонкеv_data_quality.price_disagreements_count— у всех 89 699 объявлений ровно один источник, ноль был структурно неизбежен и читался как «расхождений нет». Самиv_price_divergence/v_cross_source_healthоставлены как задел, но подписаныBROWSER_BLOCK_RESOURCESОборванная проводка, а не мёртвый код
Две находки из восьми оказались рабочими механизмами, которым не хватало соединения.
Загрузчик ДОМ.РФ — самое ценное в списке. Искать надо было не «кто удалил вызов», а «кто должен был вызвать». Планировщик диспетчеризует по паре «строка
scrape_schedules» + «Handler вproduct_handlers»; у ДОМ.РФ не было ни того, ни другого — только CLI, который однажды запустили руками:Один и тот же
loaded_atу всех строк — подпись единственного прогона. Чинится подключением: Handler + seed-строка с недельным тактом (interval_days: 7), окно 01:00-02:00 UTC — раньшеrefresh_search_matview(03:00-04:00), импорта ДКП и дневных агрегатов.filters_hash— не «Циан перестал отдавать», а «читаем не на том уровне». Проверка по сырым ответам, которые уже лежат в БД:filtersHash— соседdata, а не её содержимое. 139/139 строк несут непустой хеш, все 139 различны. Правка на один вызов + бэкфилл изraw_payload.Про переменную окружения без кода
BROWSER_BLOCK_RESOURCES=trueстоит вtradein-scraper,tradein-browserи в окружении сборки. Раскопки по истории: коммит59b5d157(#1812) заменил булев выключатель на список типовBROWSER_BLOCK_RESOURCE_TYPES.Трафик от этого не вырос, и вот почему. До #1812 блокировались
image,media,fontчерезpage.route. После —font,mediaтем же route, аimageглушится камуфоксом (block_images: True,_launch_browser, безусловно). Покрытие то же; переименовали только ручку. Измерить это ретроспективно по трафику нельзя (нет посуточных счётчиков байтов на контейнер), но по коду видно, что ни один тип ресурса не остался незаблокированным.Опасность в другом: ручка выглядит рабочей. Оператор, который поставит
BROWSER_BLOCK_RESOURCES=false, чтобы посмотреть страницу с ресурсами, ничего не выключит — и сделает вывод про блокировку, а не про переменную. Поэтому сервис теперь предупреждает на старте, а переменная остаётся инертной (реестр_RETIRED_ENV).Убрать её из окружения — задача devops (compose/
.env.runtimeвне границ этого PR).Что оставлено как задел, но подписано
v_price_divergence/v_cross_source_health— пусты структурно: боевой путь загрузки (scrapers/base.py::_link_listing_to_house) зовётupsert_listing_source('source_link')напрямую и не зовётmatch_or_create_listing(см. NOTE наmatching/listings.py:188). ВCOMMENTэто написано, чтобы «пусто» не читалось как «проверили — чисто».house_sources.ext_url— пуст 49 502/49 502, но входит в публичный контрактmarket.v_house_sources(мигр. 154), где удаление колонки объявлено ломающим изменением. Оставлен и подписан.listings.canonical— вырожден (tу всех 93 408), но читается предикатомlistings_search_mv; снос потянул бы пересоздание matview с шестью индексами ради нулевого выигрыша.Что найдено попутно и НЕ трогалось
Сканирование
pg_statsнаnull_frac = 1дало ещё кандидатов, по каждому нужен свой разбор — не смешиваю с этим PR:listings_snapshots.position_in_serp(380 007 NULL) — писательupsert_listing_snapshotпринимает параметр, но ни один вызывающий его не передаёт. Это не мёртвый код, а несоединённый: позиция в выдаче реально доступна на месте скрейпа.scrape_runs.anchor_lat/anchor_lon/radius_m/segment/target_ext_id— 3 216 прогонов, все NULL.deals.cadastral_number/days_on_market/house_type/total_floors/raw_payload— 96 974 NULL.listings_search_mv.district/distance_to_metro_m/last_price_change/photos_count— честныеNULL::placeholder'ы в DDL, но потребитель об этом не знает.tmp_purged_junk_houses_0702(3 383 строки) живёт на проде с 2 июля — это мёртвые данные, не код.loaded_atобновляется только для изменившихся строк (гейтIS DISTINCT FROMв UPSERT). Любой монитор свежести, завязанный наmax(loaded_at), объявит здоровый источник протухшим — а такой паттерн в репозитории уже есть. Предсуществующее поведение загрузчика, но актуальным становится именно сейчас, когда мы ставим его на расписание. Заводится отдельной задачей.Round 2 — правки по ревью
Признак панорамы был недостижим примерно для 10% страниц. Вызов стоял после раннего возврата по пустой истории размещений: страница, отрисованная идеально (год и этажность на месте, гейт выполнен), но без единого объявления в истории, до записи не доходила. Масштаб — 1519 оценок против 1360 домов с историей. Резолв дома и запись панорамы подняты выше возврата; гейт честности не тронут. Цена:
match_or_create_houseтеперь вызывается и для таких страниц (может создать дом) — но это тот же вызов с тем же адресом, который уже отрабатывает на остальных 90%. Дыра была вдобавок закреплена предсуществующим тестом на пустую историю — он переписан и теперь проверяет ровно то, что должен.Числа в комментариях к схеме были оценками планировщика.
reltuplesвместоcount(*): listings «142 569» против реальных 93 408 (раздув мёртвыми кортежами на 53%), house_sources «46 813» против 49 502. На безопасность удаления это не влияло — нули там точные, — но оценка уезжала в постоянный комментарий к схеме, в PR, тезис которого «каждое утверждение несёт число с прода». Пересчитано точным счётом везде: в шапке миграции, вCOMMENT ON COLUMNи здесь.Окно пересекалось с обновлением поискового представления. Обоснование окна ссылалось на импорт ДКП и дневные агрегаты, но в тех же 03:00-04:00 сидит
refresh_search_matview— ровно то задание, которое переноситyear_builtв поиск (сверено с прод-таблицейscrape_schedules). Планировщик берёт случайный момент внутри окна и гоняет источники параллельно, порядок не гарантирует ничем. Перенесено на 01:00-02:00; в комментарии честно сказано, что гарантии всё равно нет и при аномально долгом прогоне возможно отставание на цикл. Добавлен тест, который ловит откат окна обратно.Тесты, адресовавшие вызовы по позиции.
db.execute.call_args_list[0]в шести чужих тестах — это и была причина, по которой добавление второгоexecuteломало их разом. Все переведены на фильтр по SQL через общий хелпер_history_rows. То же дляside_effectв тесте отката батча: исключение доставалось бы записи панорамы (она свои ошибки глотает), и тест молча проверял бы не тот путь. Вtest_save_history_items_inserts_eachвозвращено утверждение о числе коммитов — в первой редакции оно было удалено вместо обновления, тогда как в соседнем файле обновлено; теперь симметрично.Test plan
pytest tradein-mvp/backend— 3521 passed, 9 skipped (deselect тот же, что в CI)tests/test_dead_code_sweep_2674.py— 18 тестов, разбор нижеorigin/main→ 8/18 краснеютpg_dump --schema-only, удалена после): применяется чисто, повторное применение идемпотентно,v_data_quality/market.v_house_sources/market.v_housesопрашиваются послеruff check— чисто по изменённым файламSELECT count(filters_hash) FROM external_valuations→ ожидается 139 (было 0)domrf_kapremont_loadвscrape_schedules, первый прогон в окне 01:00-02:00 UTC следующих сутокSELECT count(has_panorama) FROM houses→ должно перестать быть нулёмЧестно про тесты
Формулировка «18 тестов» звучит сильнее, чем есть. Разбор по тому, что каждый реально ловит (замерено: боевые файлы откачены на
origin/main, прогон):8 настоящих детекторов — краснеют на
origin/main: три про запись панорамы (включая новый, на страницы без истории), два про регистрацию Handler'а ДОМ.РФ, один про путьfilters_hash, один гейт удалённых дедуп-обёрток, один про предупреждение о мёртвой переменной.5 тестов только ищут текст в самой миграции (
test_domrf_loader_is_seeded_into_schedules,test_domrf_window_does_not_collide_with_matview_refresh,test_filters_hash_backfill_uses_the_same_path,test_migration_drops_exactly_what_was_declared_dead,test_price_divergence_is_documented_as_structurally_empty). Они утверждают, что файл написан так, как написан, — и не поймают миграцию, которая применится грязно или ударит не по тому объекту. Настоящая проверка здесь — прогон на копии прод-схемы, он в Test plan выше.2 вакуумных до фикса — гейты «НЕ пишем панорамы, когда страница не подтверждена»: до правки не писали вообще, так что они верны и без неё. Смысл появился вместе с записью.
2 гейта против возврата (
test_dead_names_absent_from_live_code,test_dropped_columns_have_no_python_writer) — по построению зелёные сейчас, краснеют при регрессии. 1 (test_filters_hash_absent_stays_none) документирует поведение, а не фикс — так и подписан в коде.Отдельно:
tradein-mvp/browser/test_server.pyне запускается ни в CI, ни в деплое — pytest ходит только вtradein-mvp/backend. Там же наorigin/mainуже красныйtest_pace_provider_disabled_when_zero(проверено на чистой копии main, к этому PR отношения не имеет). Поэтому поведенческие тесты про_warn_retired_envпродублированы гейтом по исходнику в backend-наборе.Refs #2674
errors=0иdone, все домовые поля Циана нулевые #2700Отложенная проверка закрыта: признак панорамы доехал и работает
При мерже третий пункт плана проверки остался в состоянии «не проверено»: код доехал, но
yandex_valuationс момента деплоя ни разу не бежал, а писать признак больше некому.Сейчас прогон состоялся:
Одна новая оценка — один заполненный признак. Ровно то, чего ждали: поле парсилось и покрывалось тестами, но в базу не попадало ни одной строкой.
Заодно закрывается и вопрос о том, была ли правка достижима для страниц без истории размещения: вызов был перенесён выше раннего возврата именно ради тех ~10%, и первая же запись это подтверждает лишь частично — одного наблюдения мало, чтобы судить о доле. Полная картина появится по мере накопления оценок.
Статус остальных двух пунктов не менялся:
cian_valuationмёртв с 29 июня из-за протухших кук (#2700), проверить парсер на живом ответе нельзя;next_run_atв окне 01:00–02:00 UTC.Загрузка ДОМ.РФ отработала по расписанию — впервые за историю продукта
Диагноз при постановке был такой: механизм написан и покрыт тестами, но не имеет ни строки расписания, ни обработчика, и вызывался только из командной строки. Признак, по которому это было опознано, — одинаковая метка времени у всех 29 978 строк, то есть единственный ручной запуск.
Сегодня в 01:00:59 расписание, засеянное миграцией 216, сработало впервые:
Диагностический признак подтвердился с обратной стороны. Он был поставлен по наблюдению «одна метка на всю таблицу»; теперь меток стало две, и вторая — ровно первый автоматический запуск. Если бы находка была неверна (например, механизм на самом деле как-то вызывался), второй метки не появилось бы, а появилась бы серия.
Работа сделана настоящая, а не холостая: 1 087 новых записей, обновлено 12 домов и 1 075 объявлений.
Почему это стоит отметить отдельно
Из трёх диагнозов, которые эпик различает — «оборванная проводка», «мёртвый код», «невыразимый механизм», — этот был отнесён к первому. Диагноз определял действие: проводку чинят (дёшево, возвращает готовую работу), мёртвый код удаляют, невыразимое документируют и убирают.
Ошибись мы здесь в сторону «мёртвого кода» — удалили бы рабочий загрузчик вместе с тестами. Ошибись в сторону «невыразимого» — оставили бы как есть навсегда.
Проверка стоила одной строки расписания и подтвердилась через сутки фактом, а не рассуждением.
Оговорка
Один успешный прогон не доказывает устойчивости. Такт недельный, следующий — 14.08; тогда и станет видно, держится ли. До тех пор корректная формулировка: «сработало один раз, как задумано».