- pluggable per-domain Repository interfaces; PostgreSQL and SQLite impls - SQLite on modernc.org/sqlite (WAL, busy_timeout, foreign_keys, immediate tx) - separate migrations/sqlite and db.MigrateSQLite; store.NewPostgres/NewSQLite - cmd/reconcile compares object storage with the journal (report, then --delete) - storage.Backend.List and photos.Repository.AllStorageKeys - DB_BACKEND/SQLITE_PATH and STORAGE_BACKEND/FS_ROOT selectable; local defaults (sqlite ./galo.db, fs ./data) - Makefile reconcile/reconcile-delete; docs and .env.example updated
5.7 KiB
Design
Context
См. proposal.md — Why. Текущее состояние, определяющее подход:
storage.Backendумеет Put/Delete/Get/URL/ServeObject/Ping/EnsureBucket/Bucket, но не умеет перечислять объекты.- Сборка мусора (
photos.Worker) управляется журналомobjects:ClaimGarbageвыбирает строки без привязок и удаляет блоб. Блобы без строки в журнале воркеру невидимы. - Ключи объектов —
objects/<object_id>/<role><ext>, слой метаданных хранит их вobjects.storage_key. - Есть два бэкенда хранения (
s3,fs) и два бэкенда метаданных (postgres,sqlite) за интерфейсамиstorage.Backendи доменнымиRepository.
Goals / Non-Goals
Goals:
- Ручная сверка хранилища и журнала: инвентаризация обеих сторон, отчёт, безопасное удаление.
- Работа с любой комбинацией бэкендов хранения и метаданных.
- Безопасное поведение по умолчанию: ничего не удаляется без явного флага, свежие блобы защищены.
Non-Goals:
- Автоматическая периодическая реконсиляция в воркере.
- Восстановление пропавших блобов (в журнале есть, в хранилище нет).
- Миграция блобов между бэкендами.
- Сверка префиксов, отличных от объектов.
Decisions
D1. Перечисление в storage.Backend
Добавляем List(ctx context.Context, prefix string) ([]ObjectInfo, error) где
ObjectInfo{Key string; Size int64; ModTime time.Time}.
- S3:
ListObjectsV2с пагинацией поContinuationToken,Prefix. - ФС: обход корня (
filepath.WalkDir), ключ — путь относительно корня, размер/время изos.Stat. - Альтернатива: не трогать интерфейс и добавить отдельный тип для реконсиляции — отвергнуто, перечисление логично принадлежит бэкенду хранения.
D2. Ключи журнала в метаданных
Добавляем AllStorageKeys(ctx) ([]string, error) в photos.Repository; реализации:
SELECT storage_key FROM objects (pg и sqlite). Контрактный набор покрывает метод.
D3. Команда cmd/reconcile
Отдельная maintenance-команда, читает конфиг, открывает выбранные бэкенды
(storage.Open + бандл репозиториев), строит множества и печатает отчёт.
Флаги: --delete (bool), --min-age (duration, по умолчанию 1h), --sample (число
примеров в отчёте). Область — фиксированный префикс objects/.
D4. Классификация расхождений
- Сироты = ключи хранилища под
objects/, которых нет в журнале; кандидаты на удаление. - Пропавшие = ключи журнала, которых нет в хранилище; только отчёт.
- Сироты моложе
--min-ageне удаляются (защита от гонки с идущей загрузкой: блоб уже записан, строка ещё не создана).
D5. Безопасность
По умолчанию — только отчёт и нулевое воздействие. Удаление только по --delete, только
под objects/, только старше --min-age. Пропавшие никогда не «исправляются».
Risks / Trade-offs
- [Гонка с загрузкой] →
--min-ageпропускает молодые блобы; рекомендуем запускать в спокойный период. - [Большие бакеты: память/время] → пагинация S3 и потоковый обход ФС; в отчёте лишь примеры, не полный список.
- [Несогласованность формата ключей между бэкендами] → оба бэкенда отдают ключи относительно корня/перфикса без ведущего слэша; контракт фиксируется тестами.
- [Удаление используемого блоба] → сверка выполняется вручную и в спокойный период; журнал — источник истины, поэтому «сирота» по определению не имеет строки.
Migration Plan
- Добавить
Listв интерфейс — S3 и ФС реализуют его (компилятор требует оба). - Добавить
AllStorageKeysвphotos.Repository— обе реализации. - Добавить
cmd/reconcile(аддитивно, ничего не меняет в работающем сервисе). - Данные не мигрируются; команда запускается вручную по необходимости.
Open Questions
- Нужно ли со временем добавить восстановление пропавших блобов или отметку повреждённых записей журнала.