fix(ptica): свежесть data-table источника считается по успешным строкам (#2956) #2957

Merged
bot-backend merged 1 commit from fix/2956-freshness-counts-failed-harvest into main 2026-08-20 08:36:06 +00:00
Collaborator

Часть #2956 — та, что чинится кодом.

Сторож молчал 24 суток

nspd_quarter_dumps — контент-таблица: harvest пишет строку и когда получилось, и когда упал. При провале harvest_error заполнен, счётчики нулевые, но fetched_at_utc всё равно проставлен. Монитор брал MAX(fetched_at_utc) без разбора — каждый провал обновлял часы свежести.

последний успешный дамп 27.07.2026 01:06:59
с 03.08 61 дамп подряд, все с harvest_error
что видел монитор возраст ~3 дня при пороге fresh_days=14ok

Слепота по построению: мерилось «записали ли мы строку», а не «получили ли мы данные».

Правильный образец — в соседней ветке того же if

Run-ledger режим отделяет успех от попытки:

MAX(COALESCE(finished_at, started_at)) FILTER (WHERE status = 'done')  AS last_success_at
MAX(started_at)                                                        AS last_attempt_at

Data-table режим не отделял ничего:

MAX(fetched_at_utc)  AS last_success_at
MAX(fetched_at_utc)  AS last_attempt_at

Правка

У FreshnessSource появилось необязательное success_where; в data-table ветке оно вешается на last_success_at. last_attempt_at остаётся MAX(ts) — это именно разделение, а не отбрасывание: признак «загрузчик ходит, но не приносит» должен остаться виден в UI.

Для nspd: success_where="harvest_error IS NULL".

Больше нигде не включаю. Из двух data-table источников колонка ошибки есть только у nspd_quarter_dumps (проверено по information_schema); gisogd_permits — чистая контент-таблица, где сама строка и есть данные, там разделять нечего.

Тест

Герметичный: все шесть таблиц реестра создаются временными в своей же сессии, поэтому он не зависит от схемы CI-базы.

В фикстуре стоит защита: сразу после создания каждая таблица обязана быть пустой. Если затенение не сработало (опечатка в имени, изменившийся реестр), запросы ушли бы в настоящую таблицу — и тест читал бы боевые данные, а его выводы были бы про них. По конвенции tests/sql/* DSN по умолчанию смотрит в туннель к проду, так что цена такой опечатки реальна.

Против кода origin/main, прогон по живому Postgres:

возраст 1.0д вместо >20           → падает  (провал обновил часы свежести)
попытка не новее успеха           → падает  (на main это одно и то же MAX(ts))
успехом сочтена строка с ошибкой  → падает  (last_success_at не None)
свежий успех по-прежнему ok       → контроль, зелёный на обеих сторонах

Прогоны

tests/sql (живой Postgres)          4 passed  rc=0
tests/workers + tests/services   3320 passed  rc=0

Четыре nodeid внесены в skip_allowlist.txt — нужен живой Postgres. В CI они идут (postgres-сервис #2745), запись нужна для машины без БД и без туннеля.

Что это НЕ чинит

Сам отказ НСПД (WAF 403 на IP VPS) остаётся — это #2956, решение владельца. Здесь чинится только то, что об отказе никто не узнавал.

Часть #2956 — та, что чинится кодом. ## Сторож молчал 24 суток `nspd_quarter_dumps` — контент-таблица: harvest пишет строку и когда получилось, и когда упал. При провале `harvest_error` заполнен, счётчики нулевые, но `fetched_at_utc` **всё равно проставлен**. Монитор брал `MAX(fetched_at_utc)` без разбора — каждый провал обновлял часы свежести. | | | |---|---| | последний успешный дамп | 27.07.2026 01:06:59 | | с 03.08 | 61 дамп подряд, все с `harvest_error` | | что видел монитор | возраст ~3 дня при пороге `fresh_days=14` → **ok** | Слепота по построению: мерилось «записали ли мы строку», а не «получили ли мы данные». ## Правильный образец — в соседней ветке того же `if` Run-ledger режим отделяет успех от попытки: ```sql MAX(COALESCE(finished_at, started_at)) FILTER (WHERE status = 'done') AS last_success_at MAX(started_at) AS last_attempt_at ``` Data-table режим не отделял ничего: ```sql MAX(fetched_at_utc) AS last_success_at MAX(fetched_at_utc) AS last_attempt_at ``` ## Правка У `FreshnessSource` появилось необязательное `success_where`; в data-table ветке оно вешается на `last_success_at`. `last_attempt_at` остаётся `MAX(ts)` — это именно **разделение**, а не отбрасывание: признак «загрузчик ходит, но не приносит» должен остаться виден в UI. Для `nspd`: `success_where="harvest_error IS NULL"`. Больше нигде не включаю. Из двух data-table источников колонка ошибки есть только у `nspd_quarter_dumps` (проверено по `information_schema`); `gisogd_permits` — чистая контент-таблица, где сама строка и есть данные, там разделять нечего. ## Тест Герметичный: все шесть таблиц реестра создаются **временными** в своей же сессии, поэтому он не зависит от схемы CI-базы. В фикстуре стоит защита: сразу после создания каждая таблица обязана быть пустой. Если затенение не сработало (опечатка в имени, изменившийся реестр), запросы ушли бы в настоящую таблицу — и тест читал бы боевые данные, а его выводы были бы про них. По конвенции `tests/sql/*` DSN по умолчанию смотрит в туннель к проду, так что цена такой опечатки реальна. Против кода `origin/main`, прогон по живому Postgres: ``` возраст 1.0д вместо >20 → падает (провал обновил часы свежести) попытка не новее успеха → падает (на main это одно и то же MAX(ts)) успехом сочтена строка с ошибкой → падает (last_success_at не None) свежий успех по-прежнему ok → контроль, зелёный на обеих сторонах ``` ## Прогоны ``` tests/sql (живой Postgres) 4 passed rc=0 tests/workers + tests/services 3320 passed rc=0 ``` Четыре nodeid внесены в `skip_allowlist.txt` — нужен живой Postgres. В CI они идут (postgres-сервис #2745), запись нужна для машины без БД и без туннеля. ## Что это НЕ чинит Сам отказ НСПД (WAF 403 на IP VPS) остаётся — это #2956, решение владельца. Здесь чинится только то, что об отказе никто не узнавал.
bot-backend added 1 commit 2026-08-20 08:16:00 +00:00
fix(ptica): свежесть data-table источника считается по успешным строкам (#2956)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 13s
CI / changes (pull_request) Successful in 13s
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 2m13s
CI / backend-tests (pull_request) Successful in 17m35s
660232ffdc
Сторож свежести молчал 24 суток, пока источник НСПД лежал.

nspd_quarter_dumps — контент-таблица: harvest пишет строку и когда получилось,
и когда упал (тогда harvest_error заполнен, счётчики нулевые, но fetched_at_utc
всё равно проставлен). Монитор брал MAX(fetched_at_utc) без разбора — и каждый
ПРОВАЛ обновлял часы свежести.

  последний успешный дамп   27.07.2026 01:06:59
  с 03.08                   61 дамп подряд, все с harvest_error
  что видел монитор         возраст ~3 дня при пороге fresh_days=14 → ok

Слепота по построению: мерилось «записали ли мы строку», а не «получили ли мы
данные». Правильный образец лежал в соседней ветке того же if — run-ledger режим
отделяет успех от попытки через FILTER (WHERE status = 'done').

Правка: у FreshnessSource появилось необязательное success_where, в data-table
ветке оно вешается на last_success_at. last_attempt_at остаётся MAX(ts) — это
именно РАЗДЕЛЕНИЕ, а не отбрасывание: признак «загрузчик ходит, но не приносит»
должен остаться виден. Для nspd success_where = "harvest_error IS NULL".

Больше нигде не включаю: из двух data-table источников колонка ошибки есть
только у nspd_quarter_dumps (проверено по information_schema). gisogd_permits —
чистая контент-таблица, где сама строка и есть данные, там разделять нечего.

Тест герметичный: все шесть таблиц реестра создаются ВРЕМЕННЫМИ в своей же
сессии, поэтому он не зависит от схемы CI-базы. В фикстуре стоит защита — сразу
после создания каждая таблица обязана быть пустой; если затенение не сработало
(опечатка в имени, изменившийся реестр), запросы ушли бы в настоящую таблицу и
тест читал бы боевые данные. По конвенции tests/sql DSN по умолчанию смотрит в
туннель к проду, так что цена такой опечатки реальна.

Против кода origin/main (прогон по живому Postgres):

  возраст 1.0д вместо >20          — провал обновил часы свежести → падает
  попытка не новее успеха          — на main это одно и то же MAX(ts) → падает
  успехом сочтена строка с ошибкой — last_success_at не None → падает
  свежий успех по-прежнему ok      — контроль, зелёный на обеих сторонах

Прогоны: tests/sql rc=0 (4 passed по живой БД); tests/workers + tests/services
3320 passed rc=0.

Четыре nodeid внесены в skip_allowlist.txt — нужен живой Postgres. В CI они
идут (postgres-сервис, #2745), запись нужна для машины без БД и без туннеля.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit f7e8228550 into main 2026-08-20 08:36:06 +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#2957
No description provided.