Files
av 5ae0c5ff81 reindex: пересборка витрины проигрыванием журнала
- `healthlog reindex` собирает витрину из журнала (тела архива + учёт
  доставок) в ОТДЕЛЬНЫЙ файл базы, строго по `(received_at, id)`; рабочую
  базу читает без наката миграций и не трогает вовсе. Подмену делает
  человек при остановленном сервисе: переименование поверх открытого
  дескриптора портит базу молча.
- Журналом считается архив, а не таблица доставок: тело без учётной записи
  заводится заново (метка из ULID, размер и хеш по распакованному телу),
  запись без тела переносится, но не сворачивается. Оракул сходимости
  встроен — два отпечатка и «объектов было/стало»; пустой журнал успехом не
  считается.
- Прогон живого архива переехал на новый пакет: второго проигрывателя
  журнала в проекте не осталось, а его утверждение о ключе сна перестало
  быть константой, протухающей с каждой доставкой.
2026-08-02 09:07:46 +03:00

56 lines
2.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package ident — единая точка генерации и разбора идентификаторов (ULID).
//
// ULID сортируется по времени создания, компактен в логах и URL (без дефисов)
// и глобально уникален across сущностей: поиск по голому id находит все
// упоминания. Канонический вид — нижний регистр.
package ident
import (
"errors"
"fmt"
"strings"
"time"
"github.com/oklog/ulid/v2"
)
// ErrInvalid — синтаксически некорректный идентификатор. Такой id трактуем
// как несуществующую сущность, без похода в БД.
var ErrInvalid = errors.New("некорректный идентификатор")
// NewID возвращает новый ULID в нижнем регистре.
func NewID() string {
return strings.ToLower(ulid.Make().String())
}
// TimeOf возвращает время создания идентификатора: UTC, секундная точность —
// ровно та форма, в которой время хранится (см. store.Now).
//
// Нужен пересборке витрины: у тела, лежащего в архиве без учётной записи,
// другого источника метки приёма нет. Дата каталога архива не годится — она
// задаёт сутки, а порядок проигрывания нужен внутри суток.
//
// `.UTC()` здесь обязателен и не для красоты: ulid.Time собирает время через
// time.Unix, то есть в локальной зоне машины, а Truncate работает с абсолютной
// длительностью и зону не нормализует. Без приведения метка подобранного тела
// сравнивалась бы с меткой из БД по-разному на разных машинах.
func TimeOf(s string) (time.Time, error) {
id, err := ulid.ParseStrict(strings.ToUpper(strings.TrimSpace(s)))
if err != nil {
return time.Time{}, fmt.Errorf("%w: %q", ErrInvalid, s)
}
return ulid.Time(id.Time()).UTC().Truncate(time.Second), nil
}
// Parse валидирует внешний идентификатор и приводит его к каноническому виду.
// Вызывается на входных границах (HTTP, CLI) до запроса к БД: сравнение строк
// в SQLite побайтовое, а base32 ULID при декодировании нечувствителен к
// регистру.
func Parse(s string) (string, error) {
id, err := ulid.ParseStrict(strings.ToUpper(strings.TrimSpace(s)))
if err != nil {
return "", fmt.Errorf("%w: %q", ErrInvalid, s)
}
return strings.ToLower(id.String()), nil
}