- `healthlog reindex` собирает витрину из журнала (тела архива + учёт доставок) в ОТДЕЛЬНЫЙ файл базы, строго по `(received_at, id)`; рабочую базу читает без наката миграций и не трогает вовсе. Подмену делает человек при остановленном сервисе: переименование поверх открытого дескриптора портит базу молча. - Журналом считается архив, а не таблица доставок: тело без учётной записи заводится заново (метка из ULID, размер и хеш по распакованному телу), запись без тела переносится, но не сворачивается. Оракул сходимости встроен — два отпечатка и «объектов было/стало»; пустой журнал успехом не считается. - Прогон живого архива переехал на новый пакет: второго проигрывателя журнала в проекте не осталось, а его утверждение о ключе сна перестало быть константой, протухающей с каждой доставкой.
12 KiB
12 KiB
1. Опоры в существующих пакетах
- 1.1
internal/ident:TimeOf(id string) (time.Time, error)— время создания из ULID, в UTC, усечённое до секунды (какstore.Now).ulid.Timeвнутри зовётtime.Unixи отдаёт локальную зону, аTruncateзону не нормализует — значит.UTC()обязателен явно. - 1.2
internal/archive: перечисление тел — обход корня, отбор форм тела, возврат относительных путей и отдельно — пропущенных файлов. Ошибка чтения каталога (включая отсутствие корня) возвращается наружу, а не превращается в пустой список; каталог не создаётся. - 1.3
internal/store:ListDeliveries(ctx)— доставки в порядке(received_at, id)со всеми фактами журнала, включаяheaders. - 1.4
internal/store: открытие рабочей базы только для чтения и без наката миграций + проверка версии схемы; расхождение — ошибка, называющая обе версии.
2. Проигрывание журнала — internal/replay
- 2.1 Собрать вход: объединить тела архива с учётом доставок; развести
четыре случая (штатная / тело без учёта / учёт без тела / файл не тело) и
отсортировать по
(received_at, id). - 2.2 Перенести учёт в базу назначения по нормированному составу: факты
журнала дословно (включая
headers), производные от разбора — пустыми (parse_status=pending,points=0,derived_layer='',uncovered_sections='[]'). Подобранные тела завести заново: id из имени файла, метка из ULID в UTC,bytes/sha256— по распакованному телу. - 2.3 Проиграть журнал последовательно:
fold.Foldпо каждой доставке,fold.Newсобирается с тем же пределом тела, что и приём (cfg.Ingest.MaxBodyMB). Отказ одной доставки не прекращает прогон, отмена контекста — прекращает. - 2.4 Собрать отчёт данными: счётчики по классам, отпечаток пересобранной витрины, признак отмены.
3. Команда healthlog reindex
- 3.1
cmd/healthlog/reindex.go: флаги--config,--out,--force; умолчание--out— сосед рабочей базы; тождество с рабочей базой проверяется по файлу (os.SameFile), а для несуществующего файла — по родительскому каталогу и имени;flag.ErrHelpне превращается вfatal. - 3.2 Жизненный цикл файла назначения: сборка под временным именем,
переименование — последний шаг успеха; при любом ином исходе файла по
пути назначения нет, временный и его спутники (
-wal,-shm) убраны. - 3.3 Отмена:
signal.NotifyContext(SIGINT, SIGTERM), частичный отчёт, ненулевой код. - 3.4
func writeReport(w io.Writer, …)— отчёт в stdout, прогресс в stderr; процедура подмены печатается только при успешном исходе. - 3.5 Коды возврата: успех — журнал непуст и свёрнута хотя бы одна доставка; ненулевой — пустой журнал, ноль свёрнутых, отмена, ошибка окружения. Отказ отдельной доставки исхода команды не меняет.
- 3.6 Подключить подкоманду в
main.go.
4. Проверки
- 4.1 Сходимость: живой приём N доставок → пересборка в отдельную базу → отпечатки совпали; второй прогон → отпечаток не изменился.
- 4.2 Порядок: журнал, у которого раскладка файлов расходится с
хронологией, даёт доставке без плотных метрик слой предшествующей по
received_at, а не соседа по каталогу; доставки одной секунды упорядочены идентификатором. - 4.3 Состав журнала: тело без учётной записи подбирается, точки доезжают,
bytes/sha256посчитаны по распакованному телу; учётная запись без тела считается и не роняет прогон; файл не-тело считается отдельно; нечитаемый каталог — отказ команды. - 4.4 Учёт в базе назначения: число строк и
headersсовпадают с рабочей плюс подобранные; проставленный в рабочей базеderived_layerна результат не влияет (отпечаток тот же, что из учёта без слоёв). - 4.5 Отказы и обратимость: битое тело не срывает прогон; отмена прекращает проигрывание и не оставляет файла назначения; рабочая база после прерванной пересборки не изменилась ни витриной, ни учётом.
- 4.6 Аргументы:
--out— тот же файл, что рабочая база, по другому пути; существующий файл без--force;--forceдаёт ту же витрину, что сборка в отсутствующий файл. - 4.7 Исход команды: пустой журнал — ненулевой код и без процедуры подмены; ноль свёрнутых — то же.
- 4.8 Отчёт не несёт значений точек, имён метрик и имён устройств.
- 4.9 Прогон живого архива переписан поверх
replay(см. 6.5), измеренные утверждения сохранены.
5. Приёмочные критерии из ревью дизайна
Рубрика порождена проходом healthlog-review-rubric до чтения предложения.
Пункты, уже закрытые разделами выше, отмечены ссылкой.
- 5.1 Идемпотентность прогона. Второй прогон даёт то же состояние не
только по отпечатку витрины, но и по учёту доставок в базе назначения
(число строк,
id,received_at,bytes,sha256подобранных тел). - 5.2 Тотальный детерминированный порядок. Результат не зависит от порядка обхода каталога, часового пояса процесса и числа перечитываний каталога (см. 4.2).
- 5.3 Независимость от «сейчас». Ни одно поле, влияющее на отпечаток, не
производно от времени прогона: метки берутся из события, а не из
store.Now. Проигрывание последовательно. - 5.4 Незавершённая сборка неотличимой от завершённой быть не может (см. 3.2, 4.5).
- 5.5 Отмена доводится до конца и различима снаружи (см. 3.3).
- 5.6 Оракул успеха не сводится к «ошибок не было» (см. 3.5, 4.7).
- 5.7 Политика частичного отказа явная и счётная. У каждого класса отказа свой счётчик, сумма счётчиков сходится с числом входов.
- 5.8 Рабочая база открывается только на чтение и без миграций (см. 1.4).
- 5.9 Расход памяти не растёт с объёмом архива. Тела читаются по одному, предел распакованного тела тот же, что у приёма (см. 2.3); превышение — учтённый отказ доставки, а не падение прогона.
- 5.10 Длинный прогон наблюдаем (см. 3.4).
- 5.11 Аргументы безопасны и обратимы (см. 3.1, 4.6).
- 5.12 Невосстановимое названо, а не досчитано. Отчёт называет классы
ожидаемого расхождения (новые доставки за время прогона, непереносимый
sealed, исправленный разбор) отдельно от самого факта расхождения. - 5.13 Ни отчёт, ни лог выше
DEBUGне несут данных о здоровье (см. 4.8).
6. Документация
- 6.1
docs/architecture.md: почему подмена базы не автоматизируется (открытый дескриптор, порча базы SQLite) и почему пересборка идёт в отдельный файл (blue-green rebuild проекции); отвергнутые варианты — с причиной. Там же — правка утверждения «пересчёт приreindexидёт по всей истории и потому точнее»: точнее не длина ряда, а исправленный разбор. - 6.2
README.md:reindexперестаёт быть «в планах», процедура применения. - 6.3
docs/plan.md:reindexвычеркнут из остатка шага «Разбор и хранилище». - 6.4 Беклог: задача удалена, заведена новая — «заголовки доставки в архиве рядом с телом» (prior art: WARC), с указанием, что без неё пересборка без рабочей базы деградирует по выводу слоя.
- 6.5
task verify:archiveпереезжает наinternal/replay: отдельной задачи прогона не заводим, второго проигрывателя в проекте не остаётся.