fix(tradein-estimator): atomic IMV link, batched yandex history, drop redundant CAST #509
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#509
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-estimator-tx-fixes"
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
3 fixes inside
services/estimator.pyfrom 2026-05-24 audit:cache_keycould overwrite each other'sestimate_idafter the main commit. New flow:INSERT trade_in_estimates → conditional UPDATE avito_imv_evaluations → single commit → log. No inner try/except (let DB errors bubble — atomicity is the point)._save_yandex_history_itemsbatched under single try/except — prior per-itemdb.rollback()destroyed the parent transaction; next iteration could run on a rolled-back session. New shape: build params first, single try/except around the loop, one outerrollback()if anything fails,commit()once on success.IS NOT NULLpredicates in_fetch_analogs—CAST(NULL AS T) IS NOT NULLis equivalent toNULL IS NOT NULLbut evaluates per-row. THEN-branch CASTs preserved (typed arithmetic /<>comparison require explicit type).Test impact
test_save_history_items_db_error_continuesintests/test_estimator_yandex_integration.pywas asserting old per-item-continue semantics (saved == 1after 1 of 2 items failed). Renamed to_rolls_back_batchand updated to assert new batch semantics (saved == 0,db.rollback.assert_called_once(),db.commit.assert_not_called()). Included in same commit.pytest tradein-mvp/backend/tests/test_estimator_*.py→ 22 passed in 0.94s locally.Test plan
POST /api/v1/trade-in/estimatewith same address (samecache_key); verifySELECT estimate_id, fetched_at FROM avito_imv_evaluations WHERE cache_key = ...resolves to the winner's estimate_id (not flapping).commit()not called._fetch_analogsSQL before/after; CAST removal should drop ~6 function calls per row (300 rows max → tiny but free win).Deep Code Review — PR #509 — verdict APPROVE
Summary
872c83c· matches headAtomicity — IMV link UPDATE (finding #4)
estimator.py:677-698— INSERT trade_in_estimates + conditional UPDATE avito_imv_evaluations now share one tx with a single COMMIT. Correct.cache_key = :ck AND (estimate_id IS NULL OR estimate_id = :estimate_id)was already idempotent — atomic move strengthens the invariant ("estimate saved ⇒ link attempted") but the race window was narrower than the description implies. Still net positive.pg_advisory_xact_lock(42, hashtext(fp)): estimator.py does NOT callmatch_or_create_housedirectly (grep confirmed); the lock fires only from scrapers writing intohouses, on a disjoint key namespace. Lock-order graph stays acyclic.avito_imv_evaluationswould log a warning and still return the estimate. New code lets DB errors bubble — a transient lock contention on the IMV row now fails the wholePOST /estimate. Acceptable trade-off since the UPDATE touches a single row keyed by UNIQUEcache_keywith PK-style locking (very low contention in practice), but worth knowing for ops.Batch tx —
_save_yandex_history_items(finding #5)estimator.py:336-408— single try/except around the loop, single rollback() / single commit(). Fixes the prior bug where per-itemdb.rollback()destroyed the parent tx and corrupted subsequent iterations.if not result.history_items: return 0— correctly avoids issuing a no-op commit on an empty batch._ext_id_stable_across_calls).for row in rows: db.execute(sql, row)could becomedb.execute(sql, rows)(SQLAlchemy 2.0 executemany) — fewer round-trips. Skip for now, scope is correctness not perf.CAST cleanup (finding #9)
estimator.py:832, 836, 851, 855— removedCAST(:target_year AS integer) IS NOT NULLand same fortarget_house_type. Correct:CAST(NULL AS T) IS NOT NULL ≡ NULL IS NOT NULL ≡ false, the cast adds nothing in IS-NULL context. THEN-branch CASTs preserved where arithmetic/<>need typed values — comment in code captures the AmbiguousParameter reasoning.Test coverage
test_estimator_yandex_integration.py:191-198— renamed_db_error_continues→_rolls_back_batch, assertssaved == 0,db.rollback.assert_called_once(),db.commit.assert_not_called(). Matches new semantics. Other 21 tests preserved.pytest tradein-mvp/backend/tests/test_estimator_*.py → 22 passed— plausible given file layout.Cross-file impact
avito_imv.py:save_imv_evaluationstill commits its own row before we re-enter estimator's atomic block — that's by design (cache write must survive even if final estimate INSERT fails downstream); the link UPDATE is a separate concern handled atomically in this PR.data/sql/030_avito_imv_cache_key_unique.sqlensures UNIQUE (cache_key) — required for the race-protection WHERE clause to mean "first writer wins".Vault cross-ref
decisions/Decision_TradeIn_DataQuality_8PR_Roadmap.md(which numbers 1-13) nor fromaudits/TradeIn_MVP_CodeReview_May23.md(C-/H-/M-/L- scheme). Suggest filing a short fix-note in vault post-merge so the audit trail isn't orphaned.Pre-flight
feat/tradein-estimator-tx-fixes— correct shape.mergeable: true, base on commit7f23a9b, merge_base099b8e4. No conflicts.--no-verify/--force/--amendsignals in PR.Verdict
Clean, focused fix. All three findings addressed precisely with no scope creep. Test renamed correctly. No security / correctness / perf regressions. Merging.