fix(ptica): отказ слоя НСПД больше не выглядит как «здесь ничего нет» (#2464-G/H) #2873

Merged
bot-backend merged 1 commit from fix/2464-nspd-grid-swallow into main 2026-08-13 17:30:32 +00:00
Collaborator

Что

NSPDClient.get_features_in_bbox_grid глушил любое исключение ячейки и возвращал []:

for r in results:
    if isinstance(r, Exception):
        logger.warning("... cell error: %s", layer_id, r)
        continue

Бан IP по WAF и падение слоя целиком становились неотличимы от честного пустого ответа.
Дальше этот [] уезжает в nspd_quarter_dumps.territorial_zones_count = 0 — «зонирования
здесь нет».

Замер на проде

nspd_quarter_dumps                                669 строк
territorial_zones_count = 0                       124
  из них parcels_count > 0                         50   ← legacy-слой данные НАШЁЛ,
                                                          а grid-слой отдал пустоту
harvest_error IS NOT NULL                          44   (40 × TimeoutError)

50 кварталов — это те, где мы точно знаем, что запрос дошёл и что-то вернул, а
grid-walk по тому же кварталу молча отдал ноль. Остальные 74 могут быть и честно пустыми —
отличить нечем, ровно в этом и дефект.

Как

Не изобретал: отзеркалил уже исправленного близнеца
app/scrapers/nspd_bulk_client.get_features_in_bbox_grid (там это чинили как «Issue #252-mirror»).

  • NspdBulkWafError → немедленный re-raise. Продолжать обход после 403 — углублять бан.
  • NspdBulkServerError считается отдельно; если 5xx были и ни одна ячейка не прошла
    бросаем вместо ложного пустого.
  • Сетевые / parse-ошибки одной ячейки по-прежнему обход не валят.

get_riasurt_sverdl_in_bbox ловит NspdBulkServerError и сохраняет прежний контракт
(«пустой list если слой пуст / упал» — так написано в его докстроке), но WafError не ловит.

Ограничение записал в коде честно: наружу из riasurt «упал» и «пусто» по-прежнему
неразличимы — у функции нет канала для флага, а менять её сигнатуру в этом PR значило бы
тащить чужой scope.

Заодно search_by_quarter (#2464-H): докстрока обещала атомарность, которой не было.
Теперь описывает, что именно бросается и где partial-success возможен — если часть
ячеек упала по сети, а часть прошла, вернётся собранное.

Проверка — тесты двусторонние

Три новых теста прогнал против nspd_client.py из main:

против main:   2 failed, 1 passed
    FAILED test_waf_403_aborts_grid_instead_of_empty_result
    FAILED test_all_cells_5xx_raises_instead_of_empty_result
    passed test_partial_5xx_keeps_data_and_does_not_raise   ← контроль
после правки:  3 passed

Третий тест — намеренный контроль обратной крайности: правка не должна превращать любую
ошибку в отказ. Он зелёный с обеих сторон, то есть проверяет не сам факт изменения.

  • pytest tests/scrapers/test_nspd_grid_walk.py — 37 passed, 1 skipped
  • pytest tests/scrapers tests/services/scrapers — 430 passed, 11 skipped
  • все потребители изменённой функции (test_nspd_client, test_riasurt_sverdl_client,
    test_nspd_sync, test_riasurt_sverdl_harvest, test_ird_harvest,
    test_opportunity_harvest, test_cadastre_bulk) — 127 passed
  • ruff check — clean

Что осталось за скобками

  • Для riasurt по-прежнему нет флага «слой упал» — отдельным заходом, если понадобится.
  • Ретроспективно перечитать 124 квартала с нулём зон правка не может: она меняет поведение
    будущих прогонов. Понадобится отдельный ре-harvest, чтобы отделить честно пустые от
    недосчитанных.

Refs #2464

## Что `NSPDClient.get_features_in_bbox_grid` глушил **любое** исключение ячейки и возвращал `[]`: ```python for r in results: if isinstance(r, Exception): logger.warning("... cell error: %s", layer_id, r) continue ``` Бан IP по WAF и падение слоя целиком становились неотличимы от честного пустого ответа. Дальше этот `[]` уезжает в `nspd_quarter_dumps.territorial_zones_count = 0` — «зонирования здесь нет». ## Замер на проде ``` nspd_quarter_dumps 669 строк territorial_zones_count = 0 124 из них parcels_count > 0 50 ← legacy-слой данные НАШЁЛ, а grid-слой отдал пустоту harvest_error IS NOT NULL 44 (40 × TimeoutError) ``` 50 кварталов — это те, где мы точно знаем, что запрос дошёл и что-то вернул, а grid-walk по тому же кварталу молча отдал ноль. Остальные 74 могут быть и честно пустыми — отличить нечем, ровно в этом и дефект. ## Как Не изобретал: **отзеркалил уже исправленного близнеца** — `app/scrapers/nspd_bulk_client.get_features_in_bbox_grid` (там это чинили как «Issue #252-mirror»). - `NspdBulkWafError` → немедленный re-raise. Продолжать обход после 403 — углублять бан. - `NspdBulkServerError` считается отдельно; если 5xx были **и ни одна ячейка не прошла** — бросаем вместо ложного пустого. - Сетевые / parse-ошибки одной ячейки по-прежнему обход не валят. `get_riasurt_sverdl_in_bbox` ловит `NspdBulkServerError` и **сохраняет прежний контракт** («пустой list если слой пуст / упал» — так написано в его докстроке), но `WafError` не ловит. **Ограничение записал в коде честно:** наружу из riasurt «упал» и «пусто» по-прежнему неразличимы — у функции нет канала для флага, а менять её сигнатуру в этом PR значило бы тащить чужой scope. Заодно `search_by_quarter` (#2464-H): докстрока обещала атомарность, которой не было. Теперь описывает, что именно бросается и где partial-success **возможен** — если часть ячеек упала по сети, а часть прошла, вернётся собранное. ## Проверка — тесты двусторонние Три новых теста прогнал против `nspd_client.py` **из main**: ``` против main: 2 failed, 1 passed FAILED test_waf_403_aborts_grid_instead_of_empty_result FAILED test_all_cells_5xx_raises_instead_of_empty_result passed test_partial_5xx_keeps_data_and_does_not_raise ← контроль после правки: 3 passed ``` Третий тест — намеренный контроль обратной крайности: правка не должна превращать любую ошибку в отказ. Он зелёный с обеих сторон, то есть проверяет не сам факт изменения. - [x] `pytest tests/scrapers/test_nspd_grid_walk.py` — 37 passed, 1 skipped - [x] `pytest tests/scrapers tests/services/scrapers` — 430 passed, 11 skipped - [x] все потребители изменённой функции (`test_nspd_client`, `test_riasurt_sverdl_client`, `test_nspd_sync`, `test_riasurt_sverdl_harvest`, `test_ird_harvest`, `test_opportunity_harvest`, `test_cadastre_bulk`) — 127 passed - [x] `ruff check` — clean ## Что осталось за скобками - Для riasurt по-прежнему нет флага «слой упал» — отдельным заходом, если понадобится. - Ретроспективно перечитать 124 квартала с нулём зон правка не может: она меняет поведение будущих прогонов. Понадобится отдельный ре-harvest, чтобы отделить честно пустые от недосчитанных. Refs #2464
bot-backend added 1 commit 2026-08-13 17:00:19 +00:00
fix(ptica): отказ слоя НСПД больше не выглядит как «здесь ничего нет»
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m0s
CI / backend-tests (pull_request) Successful in 17m12s
d43dd7d257
get_features_in_bbox_grid глушил ЛЮБОЕ исключение ячейки (warning + continue)
и возвращал []. Бан IP по WAF или падение слоя целиком становились
неотличимы от честного пустого ответа.

Замер на проде: nspd_quarter_dumps — 669 строк, у 124 territorial_zones_count=0,
и у 50 из них соседний legacy-слой данные всё-таки нашёл. То есть это не
«зон нет», а «мы не узнали».

Правка — зеркало уже исправленного близнеца в
app/scrapers/nspd_bulk_client.get_features_in_bbox_grid (Issue #252-mirror):
- NspdBulkWafError → немедленный re-raise (продолжать обход = углублять бан);
- NspdBulkServerError считаем отдельно; если 5xx были И ни одна ячейка не
  прошла — бросаем вместо ложного пустого;
- прочие (сетевые/parse) ошибки ячейки по-прежнему не валят обход.

get_riasurt_sverdl_in_bbox ловит NspdBulkServerError и сохраняет прежний
контракт («пустой list если слой пуст / упал»), но НЕ ловит WafError.
Ограничение записано в коде честно: наружу оттуда «упал» и «пусто»
по-прежнему неразличимы — у функции нет канала для флага.

Докстрока search_by_quarter обещала атомарность, которой не было; теперь
описывает, что бросается и где partial-success ВОЗМОЖЕН (#2464-H).

Тесты двусторонние: против main 2 из 3 падают
(waf_403_aborts_grid, all_cells_5xx_raises), третий — контроль
partial_5xx_keeps_data — зелёный с обеих сторон.

Один хунк форматирования — не мой: pre-commit ruff v0.7.4 против 0.15.12 (#2864).

Refs #2464
bot-backend merged commit 597429cff9 into main 2026-08-13 17:30:32 +00:00
bot-backend deleted branch fix/2464-nspd-grid-swallow 2026-08-13 17:30:33 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2873
No description provided.