fix(ptica): на worker_ready зомби помечается ЛЮБОЙ 'running', а не только со снапшотом (#2464) #2975

Merged
bot-backend merged 1 commit from fix/2464-zombie-any-running into main 2026-08-20 12:22:38 +00:00
Collaborator

Пункт эпика #2464: lifecycle.py:92.

Код противоречил собственному докстрингу

No time threshold: by definition, on worker_ready ANY 'running' row is a zombie
because there is no active worker. Previously we required heartbeat … stayed in
'running' status forever and required manual cancel/resume.

А запрос добавлял AND objects_snapshot IS NOT NULL. Строка без снапшота в выборку не попадала и оставалась 'running' навсегда — ровно то состояние, ради устранения которого функция и заводилась.

Почему фильтр там был — и почему его место не здесь

Снапшот действительно нужен: resume_kn_run восстанавливает обход «using objects_snapshot» и без него упал бы. Но он нужен для возобновления, а не для пометки.

Правка: зомби помечаются все, resume ставится только тем, кого есть чем возобновить; остальные получают честную причину в error вместо тишины.

Про тест — первая версия была негодной

Двойник сессии отдавал строки независимо от WHERE. На origin/main главный тест («строка не помечена») проходил, а краснели два других — по ложной причине: фейк игнорировал фильтр, которого весь спор и касается.

Научил двойник соблюдать ровно этот фильтр и сузил совпадение до AND objects_snapshot IS NOT NULL — правка выносит то же выражение в список полей SELECT, и матч по голой подстроке отсекал бы строки у исправленной версии тоже.

Против origin/main теперь:

строка без снапшота не помечена zombie  → падает (UPDATE вообще не выполняется)
в смешанной выборке помечены не все     → падает: {1,3} вместо {1,2,3}
невозобновляемому resume не ставится    → контроль, зелёный с обеих сторон
возобновляемый получает resume как раньше → контроль, зелёный с обеих сторон

Достижимость

Замер прода 20.08: строк в 'running' сейчас нет — правка предотвращает, а не чинит. Но из 20 исторических 'zombie' восемь без objects_snapshot, так что случай не гипотетический.

Прогоны

tests/workers   230 passed   rc=0
Пункт эпика #2464: `lifecycle.py:92`. ## Код противоречил собственному докстрингу ``` No time threshold: by definition, on worker_ready ANY 'running' row is a zombie because there is no active worker. Previously we required heartbeat … stayed in 'running' status forever and required manual cancel/resume. ``` А запрос добавлял `AND objects_snapshot IS NOT NULL`. Строка без снапшота в выборку не попадала и оставалась `'running'` **навсегда** — ровно то состояние, ради устранения которого функция и заводилась. ## Почему фильтр там был — и почему его место не здесь Снапшот действительно нужен: `resume_kn_run` восстанавливает обход «using objects_snapshot» и без него упал бы. Но он нужен для **возобновления**, а не для **пометки**. Правка: зомби помечаются все, resume ставится только тем, кого есть чем возобновить; остальные получают честную причину в `error` вместо тишины. ## Про тест — первая версия была негодной Двойник сессии отдавал строки независимо от `WHERE`. На `origin/main` главный тест («строка не помечена») **проходил**, а краснели два других — по ложной причине: фейк игнорировал фильтр, которого весь спор и касается. Научил двойник соблюдать ровно этот фильтр и сузил совпадение до `AND objects_snapshot IS NOT NULL` — правка выносит то же выражение в список полей `SELECT`, и матч по голой подстроке отсекал бы строки у исправленной версии тоже. Против `origin/main` теперь: ``` строка без снапшота не помечена zombie → падает (UPDATE вообще не выполняется) в смешанной выборке помечены не все → падает: {1,3} вместо {1,2,3} невозобновляемому resume не ставится → контроль, зелёный с обеих сторон возобновляемый получает resume как раньше → контроль, зелёный с обеих сторон ``` ## Достижимость Замер прода 20.08: строк в `'running'` сейчас нет — правка предотвращает, а не чинит. Но из 20 исторических `'zombie'` **восемь без `objects_snapshot`**, так что случай не гипотетический. ## Прогоны ``` tests/workers 230 passed rc=0 ```
bot-backend added 1 commit 2026-08-20 11:51:34 +00:00
fix(ptica): на worker_ready зомби помечается ЛЮБОЙ 'running', а не только со снапшотом (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 2m21s
CI / backend-tests (pull_request) Successful in 17m15s
991e4c28ed
Докстринг _resume_zombie_runs формулирует инвариант прямо:

    No time threshold: by definition, on worker_ready ANY 'running' row is a zombie
    because there is no active worker. Previously we required heartbeat … stayed in
    'running' status forever and required manual cancel/resume.

А запрос добавлял `AND objects_snapshot IS NOT NULL`. Строка без снапшота в выборку
не попадала и оставалась 'running' НАВСЕГДА — ровно то состояние, ради устранения
которого функция и заводилась.

Снапшот нужен, но не для пометки, а для ВОЗОБНОВЛЕНИЯ: resume_kn_run восстанавливает
обход «using objects_snapshot» и без него упал бы. Поэтому зомби помечаются все, а
resume ставится только тем, кого есть чем возобновить; остальные получают честную
причину в error вместо тишины.

Про тест — отдельно, потому что первая версия была негодной. Двойник сессии отдавал
строки независимо от WHERE, и на origin/main главный тест («строка не помечена»)
ПРОХОДИЛ, а краснели два других — по ложной причине. Научил двойник соблюдать ровно
тот фильтр, о котором спор, и сузил совпадение до `AND objects_snapshot IS NOT NULL`:
правка выносит то же выражение в список полей SELECT, и совпадение по голой подстроке
отсекало бы строки у исправленной версии тоже.

Против origin/main теперь:

  строка без снапшота не помечена zombie   → падает (UPDATE вообще не выполняется)
  в смешанной выборке помечены не все      → падает: {1,3} вместо {1,2,3}
  невозобновляемому resume не ставится     — контроль, зелёный с обеих сторон
  возобновляемый получает resume как раньше — контроль, зелёный с обеих сторон

Первый контроль ловит «починку», ставящую resume всем подряд.

Замер прода 20.08: строк в 'running' сейчас нет, то есть правка предотвращает, а не
чинит. Из 20 исторических 'zombie' восемь — без objects_snapshot, так что случай
не гипотетический.

Прогоны: tests/workers rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 18593c3019 into main 2026-08-20 12:22:38 +00:00
Author
Collaborator

Проверено на проде — и честно о том, что проверить не удалось

Деплой 18593c30131b83c2 зелёный, код в работающем воркере:

gendesign-worker-1 /app/app/workers/lifecycle.py
    # #2464: берём ЛЮБУЮ строку в 'running', без фильтра по objects_snapshot.
    SELECT run_id, (objects_snapshot IS NOT NULL) AS resumable
      FROM kn_scrape_runs
     WHERE status = 'running'

Инвариант, на котором стоит правка, проверен на проде. Формулировка «на worker_ready ЛЮБОЙ running — зомби, потому что активного воркера нет» верна только при одном воркере, а правка расширила охват пометки. Проверил:

docker ps | grep worker  →  gendesign-worker-1   (один)
docker-compose.yml       →  worker:   (без replicas/scale)

Реплик нет — инвариант держится.

Чего проверить не удалось. Эффекта на проде сейчас не видно: в kn_scrape_runs нет ни одной строки running (20 zombie, 10 done, 2 failed). Подметать нечего, значит отсутствие сирот после рестарта воркера ничего не доказывает — их не было и до него.

Подсадить синтетическую строку я отверг сознательно: пометка ставит finished_at = NOW(), а kn — критичный источник монитора свежести. Ради проверки испортить показания монитора, который я же чинил, — плохой размен.

Критерий с датой: при следующем прерывании kn-прогона (рестарт воркера во время еженедельного понедельничного сбора, 15 4 * * mon) строка обязана оказаться zombie с причиной «resume невозможен: нет objects_snapshot», а не остаться running. Ближайшее окно наблюдения — понедельник 24.08.2026. Если к 25.08 прерываний не случится, критерий переносится, а не считается выполненным.

Пока доказано: логика — двусторонним тестом, код — в контейнере, инвариант — на проде. Рантайм-эффект — нет.

Смежная находка по ходу проверки вынесена в #2978: objective_scrape_runs не подметалась вообще ничем и держит 6 строк running с 17.05 (94 суток).

## Проверено на проде — и честно о том, что проверить не удалось Деплой `18593c30`→`131b83c2` зелёный, код в работающем воркере: ``` gendesign-worker-1 /app/app/workers/lifecycle.py # #2464: берём ЛЮБУЮ строку в 'running', без фильтра по objects_snapshot. SELECT run_id, (objects_snapshot IS NOT NULL) AS resumable FROM kn_scrape_runs WHERE status = 'running' ``` **Инвариант, на котором стоит правка, проверен на проде.** Формулировка «на worker_ready ЛЮБОЙ running — зомби, потому что активного воркера нет» верна только при одном воркере, а правка расширила охват пометки. Проверил: ``` docker ps | grep worker → gendesign-worker-1 (один) docker-compose.yml → worker: (без replicas/scale) ``` Реплик нет — инвариант держится. **Чего проверить не удалось.** Эффекта на проде сейчас не видно: в `kn_scrape_runs` нет ни одной строки `running` (20 zombie, 10 done, 2 failed). Подметать нечего, значит отсутствие сирот после рестарта воркера **ничего не доказывает** — их не было и до него. Подсадить синтетическую строку я отверг сознательно: пометка ставит `finished_at = NOW()`, а `kn` — критичный источник монитора свежести. Ради проверки испортить показания монитора, который я же чинил, — плохой размен. **Критерий с датой:** при следующем прерывании kn-прогона (рестарт воркера во время еженедельного понедельничного сбора, `15 4 * * mon`) строка обязана оказаться `zombie` с причиной «resume невозможен: нет objects_snapshot», а не остаться `running`. Ближайшее окно наблюдения — понедельник **24.08.2026**. Если к 25.08 прерываний не случится, критерий переносится, а не считается выполненным. Пока доказано: логика — двусторонним тестом, код — в контейнере, инвариант — на проде. Рантайм-эффект — нет. Смежная находка по ходу проверки вынесена в #2978: `objective_scrape_runs` не подметалась вообще ничем и держит 6 строк `running` с 17.05 (94 суток).
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#2975
No description provided.