fix(tradein-matching): pg_advisory_xact_lock to prevent duplicate house INSERTs
Two scrapers calling match_or_create_house() concurrently for the same address could both miss Tier 0-3 and each INSERT a new canonical house — splitting listings across duplicate house rows and breaking aggregations. Serialize via pg_advisory_xact_lock(42, hashtext(fingerprint)). Lock is transaction-scoped (released on caller's COMMIT/ROLLBACK). After unblock, the losing scraper's Tier 2 fingerprint lookup hits the winner's alias and returns the same house_id. Closes finding #1 from 2026-05-24 trade-in audit.
This commit is contained in:
parent
02b48b8d04
commit
9a6890de6a
1 changed files with 17 additions and 8 deletions
|
|
@ -34,11 +34,11 @@ def match_or_create_house(
|
|||
) -> tuple[int, float, str]:
|
||||
"""Match existing house or create new canonical record.
|
||||
|
||||
⚠️ KNOWN LIMITATION: Concurrent calls for the same address can race —
|
||||
two scrapers calling simultaneously for an unknown address can both
|
||||
miss Tier 0-3 and each INSERT a new house row. Mitigation (Stage 8.x):
|
||||
gate scraper concurrency to 1 per fingerprint OR add
|
||||
pg_advisory_xact_lock(hashtext(normalized_address)) at function entry.
|
||||
Concurrency-safe: serializes concurrent calls for the same address fingerprint
|
||||
via pg_advisory_xact_lock(42, hashtext(fp)). The lock is transaction-scoped,
|
||||
released on the caller's COMMIT/ROLLBACK. Without this, two scrapers calling
|
||||
for an unknown address could both miss Tier 0-3 and each INSERT a duplicate
|
||||
house row. Closes finding #1 from 2026-05-24 audit.
|
||||
|
||||
Returns:
|
||||
(house_id, confidence ∈ [0.0, 1.0], method ∈ {
|
||||
|
|
@ -54,6 +54,18 @@ def match_or_create_house(
|
|||
"""
|
||||
cad = building_cadastral_number or cadastral_number
|
||||
|
||||
# Compute fingerprint early so we can acquire the advisory lock before any tier reads.
|
||||
fp = address_fingerprint(address, lat, lon)
|
||||
|
||||
# Serialize concurrent calls for the same address fingerprint to prevent
|
||||
# racing INSERTs into houses (finding #1 from 2026-05-24 audit).
|
||||
# pg_advisory_xact_lock takes (int4, int4) — namespace 42 = "tradein house matching".
|
||||
# Lock is released automatically on caller's COMMIT/ROLLBACK.
|
||||
db.execute(
|
||||
text('SELECT pg_advisory_xact_lock(42, hashtext(:fp))'),
|
||||
{'fp': fp},
|
||||
)
|
||||
|
||||
# Tier 0: cadastral number exact match
|
||||
if cad:
|
||||
row = db.execute(
|
||||
|
|
@ -83,9 +95,6 @@ def match_or_create_house(
|
|||
ext_id)
|
||||
return (house_id, 1.0, 'source_exact')
|
||||
|
||||
# Compute fingerprint once for Tier 2 and reuse for new house registration
|
||||
fp = address_fingerprint(address, lat, lon)
|
||||
|
||||
# Tier 2: address fingerprint lookup
|
||||
row = db.execute(
|
||||
text(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue