Files
healthlog/docs/tasks/items/journal-order-on-ingest.md
T
avandClaude Opus 5 a53d0f0f2f задачи: мета переехала в блок, поле «Хук» стало «Зачем»
49 файлов, миграция сделана командой tasks.py check --fix — той самой, ради
которой в скрипте оставлена читаемость старой формы. Побочно тот же прогон
проставил тег decomposed целям, у которых есть задачи: это его штатная работа.

check после миграции зелёный, индексы согласованы.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:31:29 +03:00

82 lines
6.6 KiB
Markdown

# Порядок журнала при конкурентных приёмах
- **Секция:** ядро
- **Зачем:** Решено: повторы, но после /stats. Доставка, свёрнутая раньше своей предшественницы, уходит в failed навсегда
- **Теги:** goal:journal-and-rebuild, question
**Решение принято владельцем 2026-08-02: вариант (в), но не раньше `/stats`.**
До появления наблюдаемости живём вариантом (г) с уже записанным в спеке
приёма пределом — иначе повторы лечат болезнь, которую никто не наблюдает.
Задача берётся после [наблюдаемости](stats-endpoint.md); ниже — исходная
постановка блокера, она же ТЗ.
Вынут ревью кода задачи «Разнести ответ приёма и свёртку доставки» (профиль
`deep`, враждебный проход, находка с построенным путём и прогоном).
## Вопросы
Метка `received_at` доставки фиксируется в момент выпуска ULID — **до** записи
тела в архив и до вставки строки учёта. Порядок, в котором строки становятся
видимыми воркеру, порядку меток не подчиняется: между выпуском идентификатора и
коммитом строки проходит запись тела (измерено 184 мс на 62 МиБ) плюс ожидание
занятой базы (до пяти секунд, а с повторами транзакции дольше).
Путь построен и прогнан:
1. Широкая доставка **A** автоматизации X получает `received_at = T1` и уходит
писать тело.
2. Узкая доставка **B** той же автоматизации (`T2 > T1`, только `sleep_analysis`,
плотных метрик нет) успевает закоммитить строку первой и будит воркер.
3. Воркер видит только B, сворачивает её, наследовать слой не от кого →
`ErrLayerUnknown``failed`.
4. `failed` фоновая свёртка не подбирает никогда. Точки B в витрину не попадут.
Измерено на фикстурах: живой приём даёт `B=failed` и ноль часов
`sleep_analysis/minute`; журнальный порядок — `B=parsed` и два часа. То есть
живое состояние расходится с тем, что даст `healthlog reindex`, и расхождение
молчит: уровень лога у этого исхода `WARN`, такой же, как у штатного «у этой
автоматизации плотных метрик не бывает».
**Это не регресс** — прежде свёртка шла в порядке завершения обработчиков, то
есть было хуже. Изменение окно сузило и назвало предел в спеке приёма; вопрос в
том, закрывать ли его совсем.
## Варианты и цена
**а. Резервировать строку учёта в начале `Accept`** (до записи тела), дописывая
`raw_path`/`bytes`/`sha256` после. Тогда видимость строки монотонна вместе с
`received_at`. Цена: ломается инвариант «тело на диск раньше строки учёта»,
заведённый ровно затем, чтобы не было учтённой доставки без данных; появляется
новое состояние «строка есть, тела ещё нет», которое обязаны понимать пересборка
и ретеншен.
**б. Откладывать свёртку доставки, пока она не «устоялась»** — не сворачивать
моложе N секунд. Цена: задержка N на каждую доставку и произвольное N: окно
занятости базы измерено до пяти секунд и зависит от нагрузки, так что N честно
не выбрать.
**в. `ErrLayerUnknown` в живом пути не выводит доставку из очереди**
ограниченное число повторов, потом `failed`. Цена: колонка счётчика попыток
(миграция) и политика «сколько попыток достаточно»; зато лечит и прочие случаи
«предшественница ещё не доехала». Требует правки спеки хранения («отказ разбора
`failed`»).
**г. Ничего не делать**, оставив предел названным в спеке. Цена: редкая,
молчаливая потеря точек у автоматизаций без плотных метрик; лечится
`healthlog reindex` с остановкой сервиса и ручной подменой базы, но узнать о
необходимости неоткуда — счётчика `failed` в рантайме нет.
## Что заблокировано
Ничего: задача про разнесение ответа и свёртки доведена до конца в объявленных
границах, предел записан в спеке приёма. Заблокировано только **закрытие**
предела.
Смежно: пока предел жив, полезно уметь сверять живую витрину с пересборкой —
`reindex` уже печатает оба отпечатка, но по расписанию их никто не сравнивает.
## Рекомендация
**(в)**, но не раньше `/stats`: сперва должно стать видно, сколько доставок
числится `failed` и как давно, — иначе повторы будут лечить болезнь, которую
никто не наблюдает. До тех пор — (г) с уже записанным пределом.