16 KiB
25 · Шов движок↔платформа: ратифицированная форма и почему не иначе
⚠ ЭРРАТА 10.09 (
D39.234) — слово «ЩЕДРЫЙ» про грейс стопа (строка 27) перестало быть верным, число обязано вырасти. Тело отчёта — улика и не переписывается (D23.3), поэтому поправка стоит здесь. Прилагательное было верным, пока стоп ЖЁСТКИЙ: дренаж занимал секунды, и 10 минут покрывали его с запасом. Владелец 10.09 ратифицировал две остановки (D39.234п.1): первое нажатие — мягкое, оно ДОИГРЫВАЕТ летящий вызов, а законное ожидание одного вызова у движка — доattempt_max_s1240 с против грейса 600 с (platform/internal/runner/runner.go:59). ⇒ при мягком SIGTERM нынешний грейс убивает законное ожидание, и SIGKILL не оставляет ни пометки, ни сеттла. Число едет в платформенный промт; носитель работы — строка бэклога 381.⚠ ЭРРАТА D39.156 (23.08): «поднятие потолка ФАЙЛОМ через atomic rename» (пункт ниже, дословно
потолка — файлом через atomic rename на границах юнитов) НИКОГДА НЕ РАТИФИЦИРОВАЛОСЬ. Отчёт относит этот механизм к носителям решения D39.106 — в теле D39.106 его НЕТ вовсе (проверено грепом по слайсу живого тела ноты D39.106 («ШОВ: прогон переживает деплой платформы») в../architecture/05-decisions-log.md— адресуем НОМЕРОМ, а не строкой: line-якорь в журнал решений не переживает вставку эрраты, и он уже съезжал дважды (гейт якорей, 28.08); первый хит темы — только в D39.110. ⚠ Испр. 23.08: прежняя редакция ссылалась на слайсarchive/architecture/05-decisions-D39-106-123.md, где тела D39.106 НЕТ — греп по нему подтвердил бы эррату вакуумно). Строка была КАНДИДАТОМ панели, а живой канон решил ось иначе: потолок едет со стартом прогона и после не меняется, лекарство при упоре — НОВЫЙ прогон (platform/internal/runs/reconcile.go(греп202в обработчике возобновления; номер снят 30.08 — файл правит живая сессия зоны)). ⚠ Испр. 23.08: прежняя редакция звала «строку 69», но вставка самого баннера сдвинула нумерацию — 69-я строка теперь несёт ЖИВОЙ денежный механизм («холд не освобождается по выходу юнита»), и исполнение прежней инструкции пометило бы отвергнутым НЕ ТО. Номера строк здесь больше не называем, только подстроку. Читать НАЗВАННЫЙ ВЫШЕ пункт как отвергнутого кандидата, а не как обязательный механизм; закон входной двери шва (../architecture/17-seam-inbound-law.mdп.2) это фиксирует: посреди прогона внутрь идёт ТОЛЬКО сигнал.
Что это. Независимая верификация шва и топологии: задача в переодетом домене отдана четырём чистым панелям (~18 агентов, Opus и Fable, без доступа к репозиторию; изоляция проверена поагентно по журналам). Финальная тройка: атака на готовое решение + два абстрактных дизайна, которым явно разрешили REST/gRPC/брокер. Пятое плечо — кросс-чек третьим семейством (GPT, через владельца): слепой дизайн сошёлся по форме и списку отвергнутого, атака — build with named changes без альтернативы; его уточнения помечены (GPT) ниже. Воркфлоу:
wf_6adb39d7-870·wf_2b45042d-115·wf_4ac43daf-ec5·wf_b7eae996-b9c(первый прогонwf_b2b954de-e83загрязнён чтением репозитория и отброшен; правило: файловые инструменты панелям запрещать явно, проверять постфактум).Статус: решение владельца 05.08 «прогон переживает деплой платформы» ратифицировано D39.106. Оркестратор №14.
Форма
- Два Go-модуля (движок · платформа), граница — модульная, D39.85-гард в силе.
- Движок — транзиентный/шаблонный systemd-юнит на прогон (
Restart=no, стоп SIGTERM, щедрыйTimeoutStopSec), платформа его не родитель: её деплой прогоны не трогает. - События —
events.jsonlв каталоге книги, append-only, как проекция уже закоммиченных строк SQLite движка (та же транзакция, что чекпойнт chunk×stage; outbox, не вторая запись). - Платформа тейлит журнал; запись курсора —
(engine_run_id, seq), байтовое смещение — хинт; курсор коммитится в той же Postgres-транзакции, что эффект события. - Деньги: холд + потолок движку = защита (не зависит от живости платформы), события — свежесть;
hold→capture→release.
tmctl status --json— ремонт, только по остановленному прогону (лок). - Реконсилятор — обязательный компонент: на каждый выход юнита (D-Bus-сигнал +
ExecStopPost=-маркер) и на буте, где он ПЕРЕЗАПУСКАЕТ прерванные прогоны (транзиентные юниты не переживают ребут VM). - Формат потока (NDJSON · hello · версия · seq), D39.84, запрет читать живой SQLite движка — без изменений; журнал им не является.
Отвергнутые альтернативы (кем и почему — кратко)
| Вариант | Счёт | Причина отказа |
|---|---|---|
| Монолит (движок внутри платформы) | 0 из ~18 | Радиус поражения (леджер+вход в одном адресном пространстве с часто меняющимся движком); «одна транзакция решает деньги» ложна — БД прогона + леджер + провайдер = три хранилища при любом числе процессов. Пороги пересмотра: без cgo ✅ · деплой реже месяца ИЛИ p99 ≤ 2 ч ❌ · кросс-версионный резюм в CI ❌ |
| Платформа — родитель + пайп (stdout/fd) | 0 из 15 | Время жизни движка — подмножество платформы: деплой/рестарт убивает или осиротляет прогон; дешёвый резюм это НЕ лечит (проверено отдельным плечом: факт «прерывание стоит минуты» не сдвинул ни один голос) |
| gRPC/HTTP-сервис поверх движка | 0, отвергли явно | Изобретает жизненный цикл, которого у движка нет (одна задача, эксклюзивный лок); поток всё равно умирает на рестарте платформы — долговечный буфер нужен, а он и есть журнал |
| Push в платформу (unix-сокет/localhost HTTP) | 2 голоса в одном плече, затем 0 | Платформа лежит несколько раз в неделю ПО ЗАМЫСЛУ ⇒ push требует буфер+ретраи+дедуп = журнал; плюс неаутентифицированный локальный писатель в денежные таблицы |
| Брокер (NATS/Redis/Kafka) | 0 | Третий stateful-демон на одной VM для 1 продюсера и 1 консюмера; redelivery сам создаёт риск двойного списания |
| Чтение чужой БД (в обе стороны) | 1 голос из ~18 | Сварка с приватной схемой, меняющейся еженедельно; лок-конфликт с живым писателем |
| Выделенный fd 3 вместо stdout | 0 | Никому не понадобился при свободном выборе; отдельно замерено: fd 1/2 дают SIGPIPE-смерть сироты, fd 3 — нет |
| journald/stdout как источник событий | 0 | journald молча роняет под всплеском и ротирует сам; людям — да, контракту — нет |
Подтверждено вхолодную (наши решения воспроизведены независимо)
Потолок у движка вместо синхронной авторизации · hold→capture→release · идемпотентность
(run_id, seq) · запрет читать приватную БД движка · standalone тем же путём кода (не режимом) ·
flock вместо pid-файлов · запись намерения до платного вызова (Р7) · целочисленные деньги ·
SSE, не WebSocket · status как ремонт, не поллинг.
Атака на готовое решение: вердикт build-with-changes, доминирующая альтернатива — none.
Обязательные механизмы при постройке (носители в D39.106)
Деньги:
- write-ahead intent + состояние
uncertainдля окна «платный вызов прошёл, коммит не успел»: деньги остаются НЕРЕШЁННЫМИ в эскроу — intent доказывает намерение, не факт; spend не постится без свидетельства провайдера; флаг человеку (GPT); молчаливой бесплатной работы не бывает; - холд НЕ освобождается по выходу юнита — сигнал обгоняет финальные денежные события: состояние
closing, эскроу держится до терминального seq журнала (GPT); расход сверх холда записывать полной суммой, превышение — явной строкой убытка платформы, не примечанием (GPT, уточняет PD-27); - ночная сверка с данными провайдера — приоритетнее споров об округлении: в движок суммы
входят float64 по ручной таблице цен с ненулевым якорем незнакомого слага
(
backend/internal/ledger/pricing.go:27-52), систематика процентная; - гейт потолка на каждый платный вызов, не на юнит работы;
- state-machine строки прогона в Postgres (холд может утечь до создания каталога); поднятие потолка — файлом через atomic rename на границах юнитов; кандидат: холд траншами (~часы ожидаемого расхода), не на весь прогон.
Эмиттер журнала (строка 103):
events(fsync, seq, деньги) ≠status(atomic rename, косметика, не для биллинга);- формат — развилка строки 103 (GPT): одиночный JSONL с ftruncate-ремонтом имеет гонку живого
читателя и слеп к битой записи с валидным
\n; альтернативы — пофайловые события (events/<seq>.json, temp+rename) либо CRC-фрейминг; побайтовая идентичность не требуется; - материализатор принимает только
seq == last+1под блокировкой строки прогона; тот же seq с другим payload = карантин прогона, не тихий no-op (перекрытие инстансов при деплое) (GPT); status --jsonберёт тот же flock — проверка «остановлен» не есть взаимоисключение (GPT);- пиннинг прогона к версионированному пути бинаря; резюм другой версией — только по явному флагу (движок выкатывается чаще, чем идёт прогон);
- сброс буфера на всех путях выхода (
os.Exitпропускает defer — теряются финальные события); - живость = растущий
seq, не удерживаемый лок; политика обратного давления выбирается явно.
Опс: privilege-модель создания юнитов (linger-пользователь либо узкое polkit-правило) решить до
постройки · состояние systemd — не истина (источник на буте = каталоги + Postgres) · диск ограничить
(ENOSPC на ФС леджера ломает денежные инварианты; квота на прогон, проверка места при допуске) ·
ключи идемпотентности провайдеров протухают — replay это оптимизация, не гарантия · отдача SPA из
бинаря платформы (один origin) — вопрос к П-1, не решено.
Открытое
- Строка 103 единого бэклога и денежные строки — завести при освобождении
docs/PROGRESS.mdот чужих незакоммиченных правок; задачи зонам — отдельными промтами. - Вторая машина (решение владельца: одна пока что): при переезде умирают файловая шина и
systemctl startкак глагол — заменяются хост-агентом, шлющим те же строки; outbox, деньги, идемпотентность, SSE переживают без правки. Пиннинг книги к хосту уже в П-3.