feat(tradein/cian): batch backfill task для offer_price_history + houses_price_dynamics #535
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#535
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/cian-history-backfill"
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?
What
Batch backfill для уже собранных Cian listings — iterates DB, fetches Cian detail pages for listings WITHOUT
offer_price_historyrows, persists historical price journey via existingcian_detail.save_detail_enrichment. Idempotent (skips entities already with history).Project has NO Celery infra (verified: only
app/tasks/refresh_search_matview.py+geocode_missing.pyare sync funcs called from admin endpoints). This PR mirrors that pattern — function + admin trigger, no broker bootstrap.Backend
tradein-mvp/backend/app/tasks/cian_history_backfill.py— NEW.backfill_cian_history(db, batch_size, do_listings, do_houses, dry_run) -> CianBackfillResult. Listings query:LEFT JOIN offer_price_historyanti-join. Per-iteration: fetch_detail → save_detail_enrichment → asyncio.sleep(scraper_settings.cian_delay). Counters: total, processed, succeeded, failed_fetch, failed_save, price_changes_attempted.tradein-mvp/backend/app/api/v1/admin.py— NEW endpointPOST /scrape/cian-backfill-history?batch_size=...&do_listings=...&do_houses=...&dry_run=....batch_sizecapped 1≤N≤200 viaQuery(ge=1, le=200)(prevents proxy timeouts on >200×5s runs).Houses block
Stubbed as TODO. Houses lack a
cian_zhk_urlcolumn (verified:020_houses_alter_cian.sqlonly addscian_internal_house_id bigint, no URL). To activate houses backfill: future migration065_houses_add_cian_url.sqlto add column, then populate during SERP scrape. Comment in code points to this.Why now
Predecessors:
Test plan
curl -X POST .../admin/scrape/cian-backfill-history?batch_size=10&dry_run=true→ expect listings_total > 0, listings_processed = 0 (dry-run).?batch_size=10&dry_run=false→ expect price_changes_attempted > 0 + rows in offer_price_history for those listing_ids.Deep Code Review — PR #535
Verdict: APPROVE
SHA verified:
e77bc9f(head matches prompt)Files: 2 (admin.py +48, cian_history_backfill.py +171 new) · 219 LoC
Summary
Batch backfill для
offer_price_history: iterateslistings WHERE source='cian'anti-joined againstoffer_price_history, per-listing fetch→save, idempotent throughON CONFLICT DO NOTHING(relies onoffer_price_history_listing_change_uqfrom migration 047 — verified present in main). Houses block explicitly stubbed (nocian_zhk_urlcolumn yet). No Celery — admin-triggered sync, matches existinggeocode_missing.py/refresh_search_matview.pypattern.Critical / High
None.
Medium
tradein-mvp/backend/app/api/v1/admin.py:1037-1067— nobackground=trueoption. Atbatch_size=200 × cian_delay≈5s = ~17min, single HTTP request will exceed typical Caddy/gunicorn timeouts. PeerPOST /scrape/geocode-missing-listings(admin.py:370) acceptsbackground: bool+ usesBackgroundTasks+ ownSessionLocal()for long batches. Recommend adopting same shape — non-blocking since idempotent re-runs are safe.Low
tradein-mvp/backend/app/tasks/cian_history_backfill.py:131—enrichment.price_changes or []is defensive but redundant (field(default_factory=list)guarantees list). Keep or remove, no functional impact.price_changes_attemptedcounter (line 131) counts everythingfetch_detailreturned, not whatsave_detail_enrichmentactually INSERTed past theif not change.get('change_time') or not change.get('price_rub'): continuefilter and ON CONFLICT. Name says "attempted" so OK, but logged metric ≠ row-count delta.?batch_size=Ncalls could pick overlapping rows (anti-join not under lock) → wasted HTTP fetches. ON CONFLICT saves DB integrity. Not in scope to fix, but consider an advisory_lock guard in future iteration.Positives
LEFT JOIN offer_price_history oph ON oph.listing_id = l.id WHERE oph.listing_id IS NULL+ ON CONFLICT against verified(listing_id, change_time)UNIQUE (migration 047). Re-runs only process new listings.save_detail_enrichmentdoes its owndb.commit()per listing (verified atcian_detail.py:save_detail_enrichmentfinal commit). Interrupt loses at most one in-flight row.scraper_settings.get_scraper_delay('cian')(default 5.0s) — admin can throttle without redeploy.do_houses=Trueto log skip — not a silent no-op trap.:param IS NOT NULLwithout CAST (this PR usesoph.listing_id IS NULLagainst a column, not a parameter, so no AmbiguousParameter risk — per memory rule about CAST in IS NOT NULL predicates).fetch_detailsource confirms it returnsNoneon HTTP≠200 or parse failure, and the loop explicitly handlesNone(lines 121-128) — not relying on raised exceptions like some service-layer functions in this repo silently defaulting.geocode_missing.py(dataclass result, structured logger.info summary, per-domain counters).Cross-PR context
Test plan
Manual smoke flow in PR body is reasonable. Recommend post-merge:
POST .../admin/scrape/cian-backfill-history?batch_size=5&dry_run=true→ expect listings_total reflects pending count, processed=0.POST .../admin/scrape/cian-backfill-history?batch_size=5&dry_run=false→ verify rows appear inoffer_price_history WHERE listing_id IN (...).SELECT COUNT(*) FROM offer_price_historyunchanged (UNIQUE dedup).cian_detail save failedwarnings in logs (would indicate save-path edge cases).Approving for merge (any-scope policy).