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