From 4ce5edd750021ce59363d3c587817d30624a0f2c Mon Sep 17 00:00:00 2001 From: heaven Date: Fri, 11 Sep 2026 01:15:45 +0300 Subject: [PATCH] Say why locking the run row is enough: the writer of the unit name refuses on its own once the intent is recorded, and a row that does not exist yet cannot be held. --- docs/BACKLOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 9026e081..6f156908 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -3,7 +3,7 @@ > **Единственный трекер проекта.** Здесь живут строки, на которые доки, промты и D-ноты ссылаются словами «строка N» / «строка бэклога N»: **ID строки стабилен навсегда**, не перенумеровывается и не переиспользуется (D39.80). Правки — только через оркестратора; каждая петля обязана иметь диспозицию: решено / отложено-с-записью / отклонено. > ⚠ **Это НЕ бэклог зоны `docs/`, а бэклог ПРОЕКТА.** У зон свои, с другими неймспейсами ID, и единый их строк не принимает (D39.84): платформа — [../platform/BACKLOG.md](../platform/BACKLOG.md) (`П-N`) и её регистр дефектов `platform/docs/DEFECT_REGISTER.md` (`PD-N`); фронт — [../frontend/docs/BACKLOG.md](../frontend/docs/BACKLOG.md) (`Ф-N`). > ⚠ **Состояние проекта — не здесь.** Очередь, курс, CURRENT-STATE, состояние паков и живая хроника — шапка [PROGRESS.md](PROGRESS.md). Здесь только долг и его диспозиции. -> - **СЧЁТ ОЧЕРЕДИ на 10.09 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **288** строк · зона бэкенд **138** строго / **185** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0** (236 закрыта D39.175; **371** — актом D39.233, **360** — актом D39.232, обе 10.09), платные прогоны разблокированы · «скоро» **136** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строка эксп-22: 153 (плюс 150 — руки владельца; ⚠ 55 из гейтов СНЯТА — D39.198 п.5); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). +> - **СЧЁТ ОЧЕРЕДИ на 10.09 (скриптом по таблице — `python3 docs/scripts/counts.py`; обновлять при каждом лендинге):** всего **289** строк · зона бэкенд **138** строго / **185** широко (⚠ колонки счётчик читает С КОНЦА — испр. D39.116) · **блокеров очереди 0** (236 закрыта D39.175; **371** — актом D39.233, **360** — актом D39.232, обе 10.09), платные прогоны разблокированы · «скоро» **136** (перечень — грепом по таблице, рукописный список снят D39.126) · гейт-строка эксп-22: 153 (плюс 150 — руки владельца; ⚠ 55 из гейтов СНЯТА — D39.198 п.5); строка 5 — остаток гейчен ре-пробой 74, носитель события теперь строка 188 (аудит D39.140); остальное «когда-нибудь». ⚠ Счёт — НИЖНЯЯ граница долга, не потолок (разбор — легенда таблицы ниже). > - ⚠ **Переезд 06.09 (D39.218):** таблица вынесена из `PROGRESS.md` ДОСЛОВНО и ни один ID не сдвинут — **с единственным исключением: две ячейки (строки 220 и 306) тронуты РОВНО в номере якоря `docs/scripts/counts.py:566`→`:569` и `:592`→`:595`**, потому что цель сдвинул этот же коммит; текст ячеек не менялся, длины равны. В `PROGRESS.md` на прежнем месте оставлен заголовок-указатель, поэтому ссылки «секция „Бэклог“» доезжают в один хоп. ## Бэклог (ЕДИНЫЙ, собран 26.07, актуализация 04.08 D39.99/D39.101; правки — только через оркестратора) @@ -313,7 +313,7 @@ | 389 | **ОСТАНОВЛЕННЫЙ ПРОГОН НЕ ПЕЧАТАЕТ НИЧЕГО О ТОМ, ЧТО ПРОИЗВЁЛ, и ПОЗИЦИЯ ЧИСЛИТСЯ `failed`.** Две половины одной беды — человек после остановки не понимает, в каком состоянии книга. **(а)** `backend/cmd/tmctl/main.go`, `translate()`: частичный результат рендерится только для подписного стопа и потолка; на отмене результата нет вовсе, видна одна строка ошибки. **(б)** `backend/internal/pipeline/stagerun.go`: ветвь прерванного ожидания и охранник после впуска резервации обе пишут `jobs.status = failed` за позицию, которую остановил ЧЕЛОВЕК. Ничто на `jobs.status` не гейтится (файл это и говорит) ⇒ это телеметрия, но слово читает человек. ⚠ Под инвариантом `D39.240` («правильно возобновить») это поверхность, по которой понимают, откуда продолжать | бэкенд | скоро | после остановки человек видит, что куплено и что доделано, и остановленная позиция не названа упавшей | улов движковой сессии, 10.09 | | 390 | **ПРОВАЛИВШИЙСЯ СЕТТЛ НАМЕРЕННО ОСТАВЛЯЕТ РЕЗЕРВАЦИЮ — И СЛЕДУЮЩИЙ ПОТОЛОЧНЫЙ СТОП ПРОСИТ ПОПОЛНИТЬ БОЛЬШЕ, ЧЕМ НУЖНО.** `backend/internal/pipeline/stagerun.go`, греп `THE RESERVATION IS DELIBERATELY NOT RELEASED HERE`: подметается только следующим пишущим `store.Open`. В окне между остановкой и этим открытием `reserved_usd` завышен, а нехватка для сообщения о потолке считается ИМЕННО от `reserved` ⇒ пользователю называют завышенную сумму пополнения. Решение оставить резервацию — осознанное и объявленное в коде; незаявлено ПОСЛЕДСТВИЕ | бэкенд | когда-нибудь | сумма пополнения, называемая пользователю, не зависит от того, была ли остановка между сеттлом и следующим открытием стора | улов движковой сессии, 10.09 | | 391 | ⛔ **ИЗ ЗАВИСШЕГО СВОРАЧИВАНИЯ НЕТ ВЫХОДА, ОСТАВЛЯЮЩЕГО ТЕРМИНАЛЬНЫЙ КАДР.** `backend/cmd/tmctl/main.go`, греп `signal.NotifyContext`: горутина выходит после ПЕРВОГО сигнала, регистрация снимается только отложенным `stop()`, канал ёмкостью один ⇒ второй сигнал падает в переполненный буфер и дефолтная диспозиция НЕ восстанавливается. Складывается с `D39.238`: два сигнала в одном кванте процесс видит как ОДИН, значит «нажать ещё раз» не выход даже теоретически. Единственный способ прекратить зависшее сворачивание — убийство извне, после которого платформа не получает ни исхода, ни расчёта. ⚠ **ИСПР. 11.09 — ПРЕЖНЯЯ РЕДАКЦИЯ ЭТОЙ СТРОКИ ПРОТИВОРЕЧИЛА САМА СЕБЕ, и нашла это движковая сессия, а не я.** Она обещала лечение «около двадцати строк» (вернуть дефолтную диспозицию) при критерии приёмки «выход, ОСТАВЛЯЮЩИЙ терминальный кадр». Дешёвое лечение даёт РОВНО ОБРАТНОЕ: второй сигнал убивает процесс диспозицией по умолчанию — тот же SIGKILL, только вызванный вежливее, и кадра по-прежнему нет. ⇒ **дешёвого решения у этой строки НЕТ**: выход С кадром означает «на второй сигнал сами пишем терминальную строку и выходим», и это отдельный предмет с ценой, вплотную подходящий к отменённой лестнице (`D39.240`). Состояние стора после убийства при этом ЗАПИНЕНО и корректно (`TestKillMinus9LosesAtMostOneCall`), то есть строка про ВИДИМОСТЬ исхода для платформы, а не про консистентность | бэкенд | когда-нибудь | у зависшего сворачивания есть выход, после которого платформа получает исход и расчёт — либо признано, что его цена не стоит предмета | улов движковой сессии 10.09; противоречие критерия найдено ею же 11.09 | -| 392 | ⛔ **ОБРАБОТЧИК `/stop` ШЛЁТ СИГНАЛ ПО ИМЕНИ ЮНИТА ИЗ СВОЕЙ ЖЕ ВЫПИСКИ — окно в один тик, и всё это время пользователь видит `202`, а прогон работает.** `platform/internal/runs/reconcile.go`, греп `func (s \*Service) Stop`: имя юнита приходит из `RequestStop` и уходит в `Runner.Stop`. Если между коммитом намерения и вызовом systemd свип успел РЕСТАРТОВАТЬ попытку, сигнал уходит СТАРОМУ юниту, а живёт новый. Самолечение есть — следующий проход увидит живой юнит с намерением и пере-выдаст стоп, — но **починка приходит от свипа, а не от обработчика**. ⚠ И самолечение слабее, чем кажется: пере-выдача ИНЕРТНА на юните в `deactivating` (замер `D39.237` п.2), то есть если первый сигнал не дошёл, свип этого не чинит вовсе — юнит стоит до убийства по грейсу, до десяти минут прогона, который ничего не делает, с зарезервированным холдом | платформа | скоро | остановка доходит до ЖИВОГО юнита в том же действии, которым записано намерение | улов платформенной сессии, 10.09 | +| 392 | ⛔ **ОБРАБОТЧИК `/stop` ШЛЁТ СИГНАЛ ПО ИМЕНИ ЮНИТА ИЗ СВОЕЙ ЖЕ ВЫПИСКИ — окно в один тик, и всё это время пользователь видит `202`, а прогон работает.** `platform/internal/runs/reconcile.go`, греп `func (s \*Service) Stop`: имя юнита приходит из `RequestStop` и уходит в `Runner.Stop`. Если между коммитом намерения и вызовом systemd свип успел РЕСТАРТОВАТЬ попытку, сигнал уходит СТАРОМУ юниту, а живёт новый. Самолечение есть — следующий проход увидит живой юнит с намерением и пере-выдаст стоп, — но **починка приходит от свипа, а не от обработчика**. ⚠ И самолечение слабее, чем кажется: пере-выдача ИНЕРТНА на юните в `deactivating` (замер `D39.237` п.2), то есть если первый сигнал не дошёл, свип этого не чинит вовсе — юнит стоит до убийства по грейсу, до десяти минут прогона, который ничего не делает, с зарезервированным холдом ✅ **ПОЧИНЕНО 11.09 (`00d590e`), и МЕХАНИЗМ ДОСТАТОЧНОСТИ надо записать верно — моё первое объяснение было неверным.** Лечение: `select … for update of r` в одной транзакции с записью намерения (`platform/internal/pgstore/runs.go`, греп `for update of r`, единственная строка этого коммита — прежние `for update of r, a` принадлежат другим функциям и стояли до неё). Гонка ВОСПРОИЗВЕДЕНА на живой БД в обе стороны: до правки перезапуск коммитил новое имя юнита, а ответ приходил со старым; после — с новым. ⛔ **Почему замка на строке ПРОГОНА достаточно, и почему это НЕ «заперта и строка попытки»:** опасность со стороны попытки — не изменение существующей строки, а ПОЯВЛЕНИЕ новой (имя юнита ставит заявка на спавн), а строку, которой ещё нет, замком не удержать. Существующую держит УСЛОВИЕ, а не замок: `RecordSpawn` несёт в своём `where` `and exists (… r.finished_at is null and r.stop_requested_at is null)` — заявка отказывается сама, как только намерение зафиксировано. ⇒ следующий, кто пойдёт двигать `RecordSpawn`, обязан споткнуться об это, а не о ложное «там замок». ⚠ **Остаточное окно есть и оно ДРУГОЕ:** транзакция взяла замок и прочла имя юнита ПУСТЫМ → спавн коммитится (его гард ещё видит намерение незаписанным, обычный `SELECT` замка не ждёт) → намерение коммитится. Итог: имя ПУСТОЕ, а юнит живой — это не устаревшее имя из прежней формулировки. Самолечится: `finishStopped` при пустом имени и непустой базе расхода не закрывает прогон, а спрашивает systemd и останавливает юнит — ветка написана заранее под «claim was given back but its unit exists» | платформа | скоро | остановка доходит до ЖИВОГО юнита в том же действии, которым записано намерение | улов платформенной сессии, 10.09 | | 393 | **РАСЧЁТ ПРИ ЗАВЕРШЕНИИ ИДЁТ ИЗ ДО-ДРЕНАЖНОЙ ВЫПИСКИ — класс «частично применённое состояние внутри одного прохода».** `platform/internal/runs/reconcile.go`, греп `this settlement is opportunistic`: расчёт получает ту выписку, которую проход прочитал ДО дренажа журнала; автор класс знал и закрыл ровно два поля, освежив их вручную. ⇒ любое число, которое дренаж ЭТОГО ЖЕ прохода записал в строку попытки, для расчёта невидимо. ⚠ **Честная граница, названная самой сессией: на сегодняшнем дереве это ЛАТЕНТНО, а не сломано** — расчёт не читает ни одного поля, которое пишет дренаж. Класс предъявлен исполнением только потому, что отменённый пак добавил такое поле (строка попытки видела 9 оценочных строк, выписка — 0). Пак уходит, поле уходит, **класс остаётся**. Родственник в том же файле уже стоил зоне дефекта (греп `freshRunState`) | платформа | когда-нибудь | у прохода одно состояние: расчёт не может читать выписку старше собственного дренажа | улов платформенной сессии, 10.09 | | 394 | **ПРОГОН, УБИТЫЙ ПО НАШЕМУ ЖЕ ГРЕЙСУ, ЧИТАЕТСЯ КАК ИНФРАСТРУКТУРНЫЙ СБОЙ.** Терминального кадра у него нет вовсе, маркер несёт `timeout/killed`. Разбор: исход сперва спрашивает намерение остановки и при записанном отдаёт `stopped`; БЕЗ намерения падает в причину отказа, где `timeout` даёт `service_error`. ⇒ у класса «мы сами его убили по своему сроку» нет отдельной пометки, и оператор видит поломку там, где сработала наша политика. ⚠ Прямо на предмете `D39.240`: остановка обязана быть отличима от аварии | платформа | скоро | убийство по грейсу отличимо от сбоя и названо своим словом | улов платформенной сессии, 10.09 | | 395 | **DEV И ПРОД РАСХОДЯТСЯ В ДВАДЦАТЬ РАЗ ПО ВРЕМЕНИ НА СВОРАЧИВАНИЕ, И ЭТО НИГДЕ НЕ ОБЪЯВЛЕНО.** `platform/internal/ingest/supervisor.go`, греп `WaitDelay` — тридцать секунд на dev-пути; `platform/internal/runner/runner.go`, греп `stopGrace` — десять минут на боевом. Один и тот же движок против тех же платных провайдеров получает на свёртку полминуты или десять минут в зависимости от пути запуска. Расхождение само по себе может быть законным, но оно НЕ НАЗВАНО, и dev-замер «успевает свернуться» ничего не говорит о боевом (и наоборот) | платформа | когда-нибудь | оба числа названы в одном месте с доводом, почему они разные, либо сведены | улов платформенной сессии, 10.09 | @@ -324,3 +324,4 @@ | 400 | **ПРАВКА БАНКА ЛЕГЛА, ПЛАТФОРМА УМЕРЛА ДО ЗАПИСИ О НЕЙ — СЛЕДУЮЩИЙ ПРОГОН ВОЗЬМЁТ ХОЛД И УМРЁТ НА ГАРДЕ.** `platform/internal/runs/bank.go`, греп `RecordBankMove`: ветка ошибки стора обработана (клиент пере-шлёт, сходится), а смерть ПРОЦЕССА между применением правки и записью факта канала не имеет. Цену называет собственный комментарий кода: отметка о движении банка остаётся пустой ⇒ следующий прогон допускается без пере-снапшота ⇒ умирает на снапшот-гарде уже ПОСЛЕ взятого холда (`PD-425`). ⚠ Прямо на предмете `D39.240`: «не оставить мусор» и «правильно возобновить» — это ровно оно | платформа | скоро | смерть между правкой банка и записью о ней не приводит к взятому холду и умершему прогону | разбор старшего коллеги, 10.09 | | 401 | **МУСОР НА ДИСКЕ ПОСЛЕ ОБРЫВА: три носителя, ни один не подметается.** **(а)** артефакт сборки при смерти платформы посреди неё остаётся на месте — перечень удаляемого берёт только строки с путём (`platform/internal/exports/exports.go`, греп `Unlinked`); **(б)** временные файлы движка чистит ТОЛЬКО следующая сборка того же идентификатора, которой при обрыве не будет; **(в)** частичный бэкап движка ложится под именем, которого платформа не знает (`platform/internal/runner/backup.go`). ⚠ Отдельно, инертно, но противоречит леджеру: отметка о расчёте может остаться пустой навсегда, если расчёт прошёл, а её запись — нет; читателей столбца вне тестов **0** (греп: только писатели и комментарии) | платформа | когда-нибудь | обрыв не оставляет на диске файлов, которых никто не подметёт, и отметка о расчёте не расходится с леджером | разбор старшего коллеги, 10.09 | | 402 | **ТРИ УТВЕРЖДЕНИЯ, КОТОРЫЕ ВЫНОС ХРОНИКИ В АРХИВ ОСТАВИЛ БЫ БЕЗ НОСИТЕЛЯ — заведены ТЕМ ЖЕ движением, что и вынос** (условие ока старшего коллеги на дифф, `D39.239` п.4). **(а) Стейл-упоминания снятого маркера `⟨проверить⟩` ВНЕ движка** — вынесенный отчёт называл `platform/internal/runs/spawn.go`, `platform/internal/runs/reconcile_test.go`, `platform/docs/DEFECT_REGISTER.md` (PD-277) и `eval/pilot/memory_eval.py`; все места живы сегодня, плюс `eval/pilot/kana_precision.py`. ⚠ **Стейл ли они — НЕ доказано:** в движке маркер стоит в **10** не-тестовых носителях и 7 тестовых, то есть снят он, судя по числам, только с провода инъекции. Проверять и чинить — чужим зонам (платформа, полигон), пинги им отправлены. **(б) Следствие ФЧ-7: прогон, который УПАЛ поверх потолка, приезжает `paused` с пустым кодом выхода** — по терминам «упал поверх потолка / ФЧ-7 / exit_code nil» носителей **0** в журнале решений, едином бэклоге и регистре платформы (PD-113 — исходный класс, не это следствие). Код платформы принимает это как конструкцию (греп `survives ANY exit`), но выбор «краш невидим за `paused`» нигде не записан как ПРИНЯТЫЙ. **(в) Две границы «не проверено» заланденного пака, у которых нет носителя нигде:** качество рода не измерено на живой книге; цена банкового контура в `c1` на реальной книге не мерена | доки | скоро | каждое из трёх либо получило носитель в своей зоне, либо признано неверным и снято | условие ока на вынос хроники 11.09 | +| 403 | **ЦЕПОЧКА «СВИП → ОСТАНОВКА → НАСТОЯЩИЙ systemd» НЕ ПРОЙДЕНА НИЧЕМ, и это объявлено ЗАРАНЕЕ, а не найдено потом.** Платформенная сессия дважды за смену упиралась в одно: пины говорят о ЮНИТЕ (живой systemd-стенд у зоны есть) либо о СТОРЕ (живой Postgres есть), а утверждения о поведении живут в СВИПЕ — и стенда, который проводит служебный обход через настоящую остановку настоящего юнита, в зоне нет. ⇒ две правки этой смены (`--no-block` в `560ca20` и ответ живым именем юнита в `00d590e`) доказаны каждая на своём уровне и НЕ доказаны сквозной цепочкой; так и записано в обеих секциях «что не удалось». ⚠ Заведено строкой по её же просьбе — чтобы следующий не открывал вопрос заново и не принял «пин зелёный» за «цепочка проверена». ⭐ Отдельно ценно, что предупреждение сделано ДО сдачи, а не после: сессия назвала границу своего доказательства сама | платформа | когда-нибудь | есть стенд, проводящий свип через настоящую остановку настоящего юнита, ИЛИ записано решение, что такой стенд не строится и почему | объявленная граница платформенной сессии, 11.09 |