fix(tradein/avito): reap dead 404 listings in detail backfill (don't trip block breaker) #2047
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#2047
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/tradein-avito-detail-dead-listing-reaper"
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?
Refs #1950 #1967.
Проблема (прод-замер 2026-06-28)
После перевода detail-backfill на browser-режим (Camoufox + новый прокси) прод-прогон run 458 дал
attempted=5 enriched=0 blocked=5— брейкер consecutive-blocks оборвал прогон, не дойдя до живых листингов.Root cause: очередь lat-null pending забита мёртвыми листингами (HTTP 404 — потому они и висят как coord-holes: давно сняты). В browser-режиме страница-404 («Ошибка 404. Страница не найдена») не содержит
item-view, поэтому_is_detail_soft_block(html)возвращаетTrue→fetch_detailбросаетAvitoBlockedError→ backfill считает этоblockedи инкрементит счётчик consecutive-blocks → брейкер abort. Подтверждённые мёртвые URL из run 458:kvartira-studiya_227_m_926_et._8242998446,2-k._kvartira_573_m_610_et._7872931506(обе отдают 404-страницу). В старом curl-режиме мёртвый листинг возвращал HTTP 302 →failed(брейкер не рвался), browser-режим рендерит 404-страницу → ложныйblock.Фикс
AvitoListingGoneError(avito_exceptions.py) — подклассAvitoError, НЕAvitoBlockedError/AvitoRateLimitedError, чтобы block-trap его не ловил._is_detail_not_found(html)(avito_detail.py) —Trueесли страница-404 (нетitem-viewИ в первых 16 KB есть «страница не найдена»/«ошибка 404»). Item-view-guard первым → живой листинг и анти-бот-дефлект («объявления на сайте авито» без 404-текста) →False. Проверка вставлена в browser-веткуfetch_detailПЕРЕД firewall/soft-block-чеком. Curl-путь не тронут (там 404 → non-200 →ValueError→failed).avito_detail_backfill.py): новый счётчикgone; отдельныйexcept AvitoListingGoneErrorперед block-trap — считаетgone(НЕblocked),consecutive_blocksне трогает (брейкер не рвётся), и помечает листингis_active=FALSEчерезdb.begin_nested()SAVEPOINT → мёртвый уходит из scope (snapshot фильтруетis_active=TRUE), очередь дренажится. Ложно-помеченный самозалечивается следующим SERP-свипом (re-insertis_active=TRUE).Тесты
tests/scrapers/test_avito_detail_not_found.py(new): unit_is_detail_not_found(404→True; дефлект→False; item-view→False; пусто→False) + browser-modefetch_detailна 404-html бросаетAvitoListingGoneError(неAvitoBlockedError).tests/tasks/test_avito_detail_backfill.py:test_backfill_listing_gone_marks_inactive_no_breaker— gone→gone==1,blocked==0, брейкер не abort'нул (_rotate_ipне вызван,mark_doneодин раз),UPDATE listings SET is_active=FALSEдля row id.17 passed.Cross-caller проверка (code-review)
Все вызовы
fetch_detailпрослежены:scrape_pipeline.py(browser-режим) имеет trailingexcept Exception→ новый тип безопасно поглощается вdetail_failed(net-улучшение: мёртвый 404 больше не раздуваетenrich_consecutive_blocks/не триггерит лишнюю ротацию).admin.py— curl-путь, нового типа не бросает. cian-вызовы — другой модуль.Post-deploy smoke
После деплоя — browser-mode прогон на двух подтверждённо-мёртвых URL: ожидаем
gone+is_active=FALSEвместоblocked; затем дренажpending_nolatна живых листингах.Follow-up (не в этом PR)
db.rollback()соседнего листинга может откатить накопленные gone (самокорректируется — re-detect в следующем snapshot).failed(не reaped) — мёртвые остаются (брейкер там и так не рвётся).avito_detail_backfill (mode=browser) aborted prod run 458 with attempted=5 enriched=0 blocked=5: the lat-null pending queue is dominated by DEAD listings (404 — that's why they are stale coord-holes). In browser mode a dead listing's 404 page ("Ошибка 404. Страница не найдена") has no item-view, so _is_detail_soft_block returned True → AvitoBlockedError → backfill counted it as `blocked` and the consecutive-block breaker aborted the run before reaching live listings. - New AvitoListingGoneError (subclass of common AvitoError, NOT of AvitoBlockedError/AvitoRateLimitedError) so the backfill block-trap does not catch it. - New _is_detail_not_found() detector + browser-mode branch in fetch_detail, checked BEFORE firewall/soft-block (keyed on the 404 title so it does not swallow the generic soft-block deflect). Curl path unchanged (404 → 302/ non-200 → ValueError → failed, as before). - Backfill: new `gone` counter; dedicated except AvitoListingGoneError branch that is neutral to the consecutive-block breaker and marks the listing is_active=FALSE (SAVEPOINT) so it leaves the snapshot scope. Tests: unit for _is_detail_not_found (404 True; soft-block/item-view/empty False), browser-mode fetch_detail raising AvitoListingGoneError (not Blocked), and backfill gone-row marks inactive without aborting the breaker.