Fallback REFRESH MV CONCURRENTLY: проверка подстроки 'cannot refresh materialized view' не матчит реальное сообщение PostgreSQL → recovery-ветка мёртвый код #1652

Closed
opened 2026-06-16 18:47:00 +00:00 by bot-backend · 0 comments
Collaborator

Severity: low · Категория: error-handling/dead-code · Файл: backend/app/services/site_finder/quarter_price_index_refresh.py:44-55

quarter_price_index_refresh.py:45 (_refresh_mv) и quarter_price_refresh.py:49 (refresh_quarter_price): fallback на non-concurrent refresh срабатывает только при "cannot refresh materialized view" in str(e).lower(). Реальное сообщение PostgreSQL при REFRESH CONCURRENTLY непополненной MV — 'CONCURRENTLY cannot be used when the materialized view ... is not populated' (matview.c), которое НЕ содержит подстроки 'cannot refresh materialized view' (проверено: substring-match = False). Дополнительно: этот класс ошибки (SQLSTATE 55000, ObjectNotInPrerequisiteState) SQLAlchemy оборачивает в InternalError, а except ловит только OperationalError — ветка может вообще не сработать по типу.

Импакт: задокументированный (комментарий quarter_price_index_refresh.py:32-37 'safe recovery path for first-run edge cases') graceful non-concurrent fallback фактически не работает — при первом populate с concurrently=True исключение пробросится наружу. Low: в prod для первого populate передают concurrently=False (docstring стр. 70-71 / 33), штатно ветка не задействуется; это мёртвый код, дающий ложную уверенность.

Фикс: матчить по типу/SQLSTATE — ловить (OperationalError, InternalError) и проверять getattr(e.orig, 'sqlstate', None) == '55000' либо подстроку 'is not populated'/'CONCURRENTLY cannot be used'. Применить в обоих файлах.

Почему баг: Документированный 'safe recovery path for first-run edge cases' (комментарий строк 32-37) фактически не работает: при первом populate с concurrently=True ошибка не сматчится по подстроке (а возможно и не поймается типом OperationalError) → проброс исключения вместо graceful non-concurrent refresh. Импакт ограничен ('should not happen in prod', для первого populate передают concurrently=False), поэтому low. Но recovery-ветка — мёртвый код, дающий ложную уверенность.

Как чинить: Ловить и матчить корректно: except (OperationalError, InternalError) as e: и проверять "is not populated" in str(e).lower() or "concurrently" in str(e).lower(); либо ловить psycopg.errors.ObjectNotInPrerequisiteState напрямую через e.orig.


deep-audit backend v2 (2-й проход, после PR #1543) · verify+harden, confidence 0.85 · unit B40

**Severity:** low · **Категория:** error-handling/dead-code · **Файл:** `backend/app/services/site_finder/quarter_price_index_refresh.py:44-55` quarter_price_index_refresh.py:45 (`_refresh_mv`) и quarter_price_refresh.py:49 (`refresh_quarter_price`): fallback на non-concurrent refresh срабатывает только при `"cannot refresh materialized view" in str(e).lower()`. Реальное сообщение PostgreSQL при REFRESH CONCURRENTLY непополненной MV — 'CONCURRENTLY cannot be used when the materialized view ... is not populated' (matview.c), которое НЕ содержит подстроки 'cannot refresh materialized view' (проверено: substring-match = False). Дополнительно: этот класс ошибки (SQLSTATE 55000, ObjectNotInPrerequisiteState) SQLAlchemy оборачивает в InternalError, а except ловит только OperationalError — ветка может вообще не сработать по типу. Импакт: задокументированный (комментарий quarter_price_index_refresh.py:32-37 'safe recovery path for first-run edge cases') graceful non-concurrent fallback фактически не работает — при первом populate с concurrently=True исключение пробросится наружу. Low: в prod для первого populate передают concurrently=False (docstring стр. 70-71 / 33), штатно ветка не задействуется; это мёртвый код, дающий ложную уверенность. Фикс: матчить по типу/SQLSTATE — ловить `(OperationalError, InternalError)` и проверять `getattr(e.orig, 'sqlstate', None) == '55000'` либо подстроку 'is not populated'/'CONCURRENTLY cannot be used'. Применить в обоих файлах. **Почему баг:** Документированный 'safe recovery path for first-run edge cases' (комментарий строк 32-37) фактически не работает: при первом populate с concurrently=True ошибка не сматчится по подстроке (а возможно и не поймается типом OperationalError) → проброс исключения вместо graceful non-concurrent refresh. Импакт ограничен ('should not happen in prod', для первого populate передают concurrently=False), поэтому low. Но recovery-ветка — мёртвый код, дающий ложную уверенность. **Как чинить:** Ловить и матчить корректно: `except (OperationalError, InternalError) as e:` и проверять `"is not populated" in str(e).lower() or "concurrently" in str(e).lower()`; либо ловить psycopg.errors.ObjectNotInPrerequisiteState напрямую через e.orig. --- <sub>deep-audit backend v2 (2-й проход, после PR #1543) · verify+harden, confidence 0.85 · unit B40</sub>
bot-backend added the
week ревью 1
label 2026-06-16 18:47:00 +00:00
Sign in to join this conversation.
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#1652
No description provided.