fix(cadastre): remove shared phase_state early-exit (race condition root-cause) #183
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#183
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/cadastre-remove-shared-phase-state"
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
ROOT CAUSE FIX for low cadastre coverage. Revealed by Fix C (#182) metrics — pilot v7 made only 5 NSPD requests for 50 quarters because 95% of parallel Celery workers early-exited without doing any work.
Root cause
`cadastre_jobs.phase_state` is shared between ALL parallel workers of the same job (one row per job_id). The early-exit in `harvest_quarter`:
```python
if phase_state.get("phase") == "done":
return result
```
triggered as soon as the FIRST worker completed its quarter (Phase 4 → progress_cb writes `{"phase": "done"}`). All other workers (concurrently running and re-reading same shared row) saw `phase=done` and bailed before Phase 1.
Effect:
Fix
Delete the early-exit entirely. Idempotency is already guaranteed by `ON CONFLICT DO UPDATE` in every `upsert_*` function. If a Celery task is re-enqueued (acks_late + retry), it re-runs harvest_quarter and writes the same rows — no duplicates.
Cost: slight extra NSPD calls on re-enqueue (rare). Benefit: correct execution for ALL quarters in concurrent jobs.
Tests
Test plan
Closes part of #168 (root-cause).
Review
CI зелёный (backend 1m9s + frontend 1m41s) + cross-check (agent):
✅ Race diagnosis правильный
cadastre_jobs.phase_stateper-job_id (single row keyed byjob_id):scrape_cadastre.py:84—phase_state = COALESCE(phase_state, '{}') || :update(JSONB merge на ту же row)scrape_cadastre.py:109—UPDATE cadastre_jobs SET ... WHERE job_id = :idbulk_harvest.py:118—WHERE job_id = :idбез quarter filterКаждый parallel worker для того же job читает/пишет ту же row → race реальна. Объясняет pilot v7 (5 req / 50 quarters = 0.1/q) и job #8 (4950 / 2408 ≈ 2/q): первый worker заканчивал quarter → progress_cb писал
phase=doneв shared row → все остальные workers читали и бейлились до Phase 1.✅ Idempotency подтверждена — все 8 upsert_* имеют ON CONFLICT
upsert_parcel(cad_num) DO UPDATEupsert_building(cad_num) DO UPDATEupsert_construction(cad_num) DO UPDATEupsert_onc(cad_num) DO UPDATEupsert_enk(cad_num) DO UPDATEupsert_zouit(reg_numb_border, category_id) DO UPDATEupsert_quarter_stats(cad_number) DO UPDATEupsert_quarter_geom_from_featureZero plain INSERT без conflict clause. Re-enqueued task safe.
✅ Resumability tradeoff приемлем
Удалённый early-exit давал ложную resumability (он не работал из-за race). Реальный recovery layer — Celery acks_late+retry. Worst case re-enqueue → лишние NSPD calls, bounded
_SEMAPHORE(3)+_MAX_RETRIES=3.✅ Test correctly inverted
test_harvest_quarter_does_not_early_exit_on_shared_phase_doneмокает sharedphase_state = {"phase": "done", "quarter": "66:41:9999999"}(от другого quarter), assert'ит:search_by_quarter.assert_awaited_once_with(...)— work выполняетсяresult.phase_state == {"phase":"done","quarter":"66:41:0303161"}— этот worker пишет СВОЙ quarter, не сохраняет sharedСильный regression shield.
✅ Drift отсутствует
HarvestResult.phase_stateunchanged.update_progresscallback unchanged. Только internal behaviorharvest_quarterменяется. No TS/pydantic.Контекст
Этот PR — root cause fix для всей серии #175–#182. После deploy pilot v8 должен дать:
requests_countтеперь точно отражает реальную нагрузку (Fix #182)approve merge