feat(tradein): city sweep — auto ЕКБ pipeline (anchors × pages + enrichment + cancel) #477
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#477
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-avito-city-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
Auto full city sweep для AvitoScraper v2: iterate 5 anchors ЕКБ × N pages → save listings → enrich houses + detail top-N. Async background execution (FastAPI BackgroundTasks), cancel mid-flight через
scrape_runs.status='cancelled', configurable delay, daily cron с random 0-3h delay для anti-pattern detection.LOC: ~+500 (5 files modified/new + 1 migration + 1 cron + 1 test file).
Changes
Backend
avito.py:fetch_around(pages=N, delay_override_sec=N)— pagination 1..N (backward compat default=1)scrape_pipeline.py:EKB_ANCHORSconst (5 точек, matchescron-scrape.sh)CitySweepCountersdataclass (anchors_done/total + per-step aggregates + errors_count)run_avito_city_sweep(db, *, run_id, pages_per_anchor, detail_top_n, request_delay_sec, ...):scrape_runs.py: utility module (create_run, update_heartbeat, mark_done/failed/cancelled, is_cancelled, list_recent)admin.py— 3 NEW endpoints:POST /admin/scrape/avito-city-sweep— start (returns run_id, BackgroundTasks)GET /admin/scrape/avito-city-sweep/runs?limit=N— list recent (для UI polling)POST /admin/scrape/avito-city-sweep/{run_id}/cancel— cooperative cancel051_scrape_runs_extend.sql: 015 не имелparams,counters,error,finished_at+cancelledв status enum + дефолт дляrun_type. Migration добавляет.tests/test_city_sweep.py— 16 offline tests passCron
deploy/avito-city-sweep.sh— wrapscurlк admin endpoint с random delay 0-3h0 2 * * * /opt/gendesign/tradein-mvp/deploy/avito-city-sweep.sh→ реальный запуск 02:00-05:00 UTC случайно каждый деньVerify
test_city_sweep.py16/16 passtest_api_avito_stage4a.py5/5 pass (no regression)Deviations
051вместо031(031 уже занят в схеме)run_typeв 015 былNOT NULLбез default → 051 добавляетDEFAULT 'city_sweep'чтобыcreate_runне падалScrapeRequest(admin.py) попутно исправлены — комментарии перенесеныSessionLocal(sync) для background task — соответствует pattern всего codebaseAuth
Все 3 endpoints без require_admin (consistent с PR #474 — Caddy basic_auth достаточен).
Test plan
scrape_runsenum has 'cancelled' + new columns/admin/scrape/avito-city-sweep {pages_per_anchor: 1, detail_top_n: 5}→ verify run_id returned + background task progresses через/runspolling/cancel/{run_id}→ verify status →cancelledв течение следующего anchorFrontend contract
Frontend PR (parallel) wires
/scrapers/avitoUI к этим 3 endpoints с polling каждые 5s.Refs
- avito.py: fetch_around(pages=N, delay_override_sec) — pagination 1..N (backward compat default=1) - scrape_pipeline.py: * EKB_ANCHORS const (5 точек ЕКБ) * run_avito_pipeline: pages + request_delay_sec params (backward compat defaults) * CitySweepCounters dataclass с anchors_done/total + aggregate counters * run_avito_city_sweep(db, run_id, *, pages_per_anchor, detail_top_n, request_delay_sec) * Cooperative cancel: checks scrape_runs.status каждый anchor * Per-anchor heartbeat + counters refresh - scrape_runs.py NEW: utility module (create_run/update_heartbeat/mark_done/failed/cancelled/list_recent/is_cancelled) - admin.py: 3 NEW endpoints: * POST /admin/scrape/avito-city-sweep — start (returns run_id, FastAPI BackgroundTasks) * GET /admin/scrape/avito-city-sweep/runs?limit=N — recent runs list * POST /admin/scrape/avito-city-sweep/{run_id}/cancel — cooperative cancel - 051_scrape_runs_extend.sql: ADD COLUMN params/counters/error/finished_at + cancelled в status enum - avito-city-sweep.sh: cron wrapper с random 0-3h delay (anti-pattern-detection) - tests/test_city_sweep.py: 16 offline smoke tests (counters, scrape_runs utils, endpoints) Crontab entry: 0 2 * * * /opt/.../avito-city-sweep.sh (runs 02:00-05:00 UTC daily)Merged via deep-code-reviewer — verdict APPROVE.
Key safety findings
Migration 051 (CRITICAL focus)
statusхранится как TEXT + CHECK constraint (не PostgreSQL ENUM type) — концернALTER TYPE ADD VALUEinside transaction N/A. DROP/ADD CHECK работает в BEGIN/COMMIT ✅ADD COLUMN IF NOT EXISTSдля всех 4 столбцов — идемпотентно ✅ALTER COLUMN run_type SET DEFAULT 'city_sweep'— affects future inserts only, existing rows untouched ✅Cooperative cancel race (CRITICAL focus)
mark_done→mark_cancelledsucceeds (WHERE status='running') → status=cancelled →mark_doneUPDATE сWHERE status='running'matches 0 rows → cancelled status persists ✅AND status='running'guard в mark_done/mark_failed.Backward compat
fetch_around(pages=1)default — existing callers (run_avito_pipelinefrom non-sweep paths) не affected ✅Contract stability для PR #478
Все 3 endpoints stable:
POST /admin/scrape/avito-city-sweep→{run_id, status, pages_per_anchor, detail_top_n}GET /admin/scrape/avito-city-sweep/runs?limit=N→[{run_id, source, status, params, counters, error, started_at, finished_at, heartbeat_at}](ISO strings или null)POST /admin/scrape/avito-city-sweep/{run_id}/cancel→{ok, run_id, cancelled}Frontend PR #478 готов к merge после backend deploy confirmed live (миграция 051 applied + endpoint smoke).
Follow-ups (отдельным PR, не блокирующее)
status='running'навсегда. Добавить cron job:UPDATE scrape_runs SET status='zombie' WHERE status='running' AND heartbeat_at < NOW() - INTERVAL '2 hours' AND source='avito_city_sweep'. Indexscrape_runs_zombies_idxуже есть (015).random.shuffle(_anchors).Post-merge verify checklist
deploy-tradein.ymltriggers (paths backend + deploy + data/sql)\d+ scrape_runs→ естьparams jsonb,counters jsonb,error text,finished_at timestamptz+ CHECK включает'cancelled'POST /admin/scrape/avito-city-sweep {pages_per_anchor:1, detail_top_n:5}→ run_id returned, через 60s/runsпоказывает progress