Идентичность на ULID: download_infohash, guarded-дедуп, миграция (ulid-identity)
Все сущности переехали с INTEGER AUTOINCREMENT на TEXT ULID (lowercase, internal/ident — единая точка генерации и разбора; oklog/ulid). Инфохэши загрузки — множество (download_infohash, v1/v2 гибридных торрентов): дедуп и сопоставление в поллинге по любому из хешей, magnet-парсер отдаёт оба хеша гибридной ссылки, усечённый v2-хеш v2-only раздач не хранится. Инвариант «не более одной активной загрузки на infohash» вместо снятого unique-индекса держат guarded-методы store в одной write-транзакции (_txlock=immediate): CreateDownloadIfNoActive (приём/adopt, с доносом недостающих хешей), ActivateIfNoOtherActive (retry/recovery/relink, отказ до побочных эффектов), guarded AddInfohashes; SetDownloadState отклоняет терминал→активное как механический бэкстоп. Миграция 0006 — первая Go-миграция goose: пересоздание таблиц при включённых FK, backfill ULID с timestamp из created_at (хронология id сохранена), разнос infohash, удаление idempotency_key. BREAKING: формат id в URL/логах/Telegram, REST-поля id (string) и infohashes (список). Новая конвенция docs/conventions/database.md (без числовых PK), корреляция в логах grep'ом по голому ULID, ER-схема обновлена. Спеки: новая capability identity, MODIFIED в state-reconciliation; change заархивирован. Пройдены ревью дизайна и кода (по 8 углов), все находки исправлены с регрессионными тестами. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
## 1. Фундамент: пакет ident
|
||||
|
||||
- [x] 1.1 Добавить зависимость `github.com/oklog/ulid/v2`; пакет
|
||||
`internal/ident`: `NewID()` (lowercase, monotonic entropy,
|
||||
потокобезопасно), `NewIDAt(t time.Time)` (для миграции/бэкфилла),
|
||||
`Parse(s)` (нормализация регистра + валидация); тесты
|
||||
(lowercase, сортируемость, отказ на мусоре)
|
||||
|
||||
## 2. Схема и миграция
|
||||
|
||||
- [x] 2.1 Механизм Go-миграций goose в `internal/store/migrations`
|
||||
(регистрация через `goose.AddMigrationContext`, совместный прогон с
|
||||
embed SQL-миграциями)
|
||||
- [x] 2.2 Миграция 0006: новые таблицы с TEXT ULID PK (все шесть), перенос
|
||||
данных с маппингом `int → ULID` (timestamp из `created_at`, fallback —
|
||||
время миграции), перенос `download.infohash` → `download_infohash`
|
||||
(lowercase, `kind` по длине hex), удаление `download.infohash` и
|
||||
`download.idempotency_key`, пересоздание индексов
|
||||
- [x] 2.3 Тест миграции на фикстурной БД: FK-связи сохранены, порядок по
|
||||
`id` = порядок по `created_at`, хеши разнесены, `idempotency_key`
|
||||
отсутствует
|
||||
|
||||
## 3. Store
|
||||
|
||||
- [x] 3.1 Типы id `int64 → string` во всех структурах и методах `store`
|
||||
(download, recognition, hint, override, metadata_candidate,
|
||||
file_link, list); генерация ULID через `ident.NewID()` во ВСЕХ
|
||||
Create-методах (вместо `LastInsertId`)
|
||||
- [x] 3.2 Guarded-методы инварианта (design D4):
|
||||
`CreateDownloadIfNoActive` и `ActivateIfNoOtherActive`, каждый — одна
|
||||
write-транзакция; `FindActiveByInfohash`/`ExistsByInfohash` join'ом
|
||||
по любому хешу (для чтения); убрать CASE-восстановление
|
||||
`idempotency_key` из `SetDownloadState`
|
||||
- [x] 3.3 Методы хешей: добавить недостающие хеши загрузке
|
||||
(INSERT OR IGNORE), получать хеши вместе с Download (срез в модели)
|
||||
- [x] 3.4 Поиск в списке (`listWhere`): `EXISTS`-подзапрос по
|
||||
`download_infohash` вместо `LIKE` по удаляемому `download.infohash`
|
||||
|
||||
## 4. Ядро и воркер
|
||||
|
||||
- [x] 4.1 Приём (`ingest`) и discover-adopt — через
|
||||
`CreateDownloadIfNoActive`; хеш из magnet (btih ИЛИ btmh, `kind` по
|
||||
длине hex) пишется в той же транзакции; сигнатуры
|
||||
`Result.DownloadID`, `notifyFailed`, `Notifier.Notify`,
|
||||
`failNotified` — на string
|
||||
- [x] 4.2 Поллинг/сверка: сопоставление раздачи по любому из хешей загрузки
|
||||
(Poll, desync, recovery, preflight); дописывание недостающих v1/v2,
|
||||
когда qBittorrent отдаёт оба; scoped-логгеры — первый известный хеш
|
||||
- [x] 4.3 Retry, recovery-воскрешение и Relink — через
|
||||
`ActivateIfNoOtherActive` (сейчас гонку закрывал unique-индекс —
|
||||
см. design D4); понятная ошибка при занятом хеше
|
||||
- [x] 4.4 `apply_batch_id` генерировать через `ident.NewID()`
|
||||
|
||||
## 5. Внешние границы
|
||||
|
||||
- [x] 5.1 `httpapi`: `ident.Parse` в `pathID` (невалидный → 404 без похода
|
||||
в БД) и для `candidate_id` из формы ревью; сентинелы
|
||||
`downloadID > 0 → != ""`; REST DTO: `id` string, `infohash` →
|
||||
список `infohashes`; карточка показывает все хеши с копированием;
|
||||
live-лукап по любому хешу; проверить шаблоны и ссылки
|
||||
- [x] 5.2 Telegram (`tgbot`): `parseCallback` и callback-data на string-id,
|
||||
`pending map[int64]int64 → map[int64]string`, сентинел `id == 0 →
|
||||
== ""`, ссылки `/review/{id}`; понятный ответ на устаревшую
|
||||
callback-data со старым числовым id
|
||||
|
||||
## 6. Логи и документация
|
||||
|
||||
- [x] 6.1 Атрибуты `<entity>_id` в логах: `recognition_id` у попыток
|
||||
распознавания, `batch_id` у раскладки; сверить с scoped-логгером
|
||||
- [x] 6.2 `docs/conventions/logging.md`: примеры id в формате ULID, grep по
|
||||
голому id как штатная корреляция
|
||||
- [x] 6.3 Новая `docs/conventions/database.md` (TEXT ULID PK, без
|
||||
AUTOINCREMENT, естественные ключи у деталей, lowercase + нормализация);
|
||||
ссылки из `docs/conventions/README.md` и `CLAUDE.md`
|
||||
- [x] 6.4 ER-схема `docs/specs/database.md`: ULID PK, `download_infohash`,
|
||||
удалённые столбцы
|
||||
|
||||
## 7. Проверка
|
||||
|
||||
- [x] 7.1 `task test` и `task lint` зелёные; ручной прогон: приём magnet →
|
||||
дедуп повторного приёма → страница `/download/{id}` с ULID в URL
|
||||
Reference in New Issue
Block a user