- `healthlog reindex` собирает витрину из журнала (тела архива + учёт доставок) в ОТДЕЛЬНЫЙ файл базы, строго по `(received_at, id)`; рабочую базу читает без наката миграций и не трогает вовсе. Подмену делает человек при остановленном сервисе: переименование поверх открытого дескриптора портит базу молча. - Журналом считается архив, а не таблица доставок: тело без учётной записи заводится заново (метка из ULID, размер и хеш по распакованному телу), запись без тела переносится, но не сворачивается. Оракул сходимости встроен — два отпечатка и «объектов было/стало»; пустой журнал успехом не считается. - Прогон живого архива переехал на новый пакет: второго проигрывателя журнала в проекте не осталось, а его утверждение о ключе сна перестало быть константой, протухающей с каждой доставкой.
56 lines
2.9 KiB
Go
56 lines
2.9 KiB
Go
// 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
|
||
}
|