Files
EventHubSpec/ARCHIVE.md
T

116 lines
8.5 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# Архив календаря (Фаза 3, волна 1)
Канон хранения и чтения истории. Трекер: [Spec#20](https://git.sabilin.com/EventHub/EventHubSpec/issues/20), эпик [Spec#19](https://git.sabilin.com/EventHub/EventHubSpec/issues/19).
Реализация: Back#76, Front#73. Этот документ — контракт; код подтягивается после него.
## Цель
Горячая Mnesia и live-запросы **не растут с годами истории**. Прошлые месяцы остаются usable в том же React-grid, без iframe как основного UX.
Критерий выигрыша: размер и нагрузка **рабочей** БД (dump/checkpoint, репликация `disc_copies`, `index_read` календаря), не «ещё одна копия тех же строк в другой таблице/ноде».
## Почему не старый pipeline (#15)
Старый pipeline (`archive_controller` / `archive_manager` / extra BEAM / HTML `GET …/view`):
- не планировался и не переносил данные (типы дат, схема `#event{}` разъехалась);
- extra BEAM на день/месяц — **больше** схем и load, не меньше;
- HTML `/view` и renderer **удалены**: история только через JSON `GET …/events`.
Extra-node, `slave`/`peer` для архива и построчный `event_archive` **удалены** (не «мёртвый» код).
## Модель: три яруса, ключ месяц
Единица архива: **календарь × `YYYY-MM`**. Не строка события.
| Ярус | Где | Рост | Кто читает |
|------|-----|------|------------|
| **Hot** | `event`, `booking` (`disc_copies`) | **текущий календарный месяц + всё будущее** | живой grid; выборка по `from`/`to`, не весь календарь |
| **Warm** | `month_snapshot` (`disc_only_copies`), ключ `{calendar_id, year_month}` | **3 закрытых месяца** gzip JSON | частый flip на прошлый месяц — один lookup |
| **Cold** | `{UPLOAD_DIR}/archive/{calendar_id}/{YYYY-MM}.json.gz` | диск volume, не Mnesia | тот же JSON API |
### Оценка нагрузки (зачем не 90 дней)
Живые запросы Front:
| Путь | Окно |
|------|------|
| Month grid | ~42 дня viewport (хвосты соседних месяцев) |
| Week / ghost | 714 дней |
| Time Arc hints | сегодня + **21 день** вперёд |
| Day | 1 день |
90 суток lookback в hot **не обслуживает** ни один live-путь: прошлый календарный месяц в UI — уже «архив». Пока строки висят в `event`, `dirty_index_read(calendar_id)` (как сейчас) тащит их на каждый `GET /events`.
Оценка busy-студии (8 слотов × 8 мастеров ≈ 64 события/день), **только lookback** (будущее всё равно в hot):
| Lookback | Лишние строки в hot на календарь |
|----------|----------------------------------|
| 90 суток | ~5800 |
| текущий месяц (~17–31 день) | ~11002000 |
| закрытый месяц после grace | **0** исторических месяцев |
100 таких студий: 90д lookback ≈ **0.5M** лишних hot-строк в `disc_copies` на **каждый** узел кластера. Warm-blob 24 месяцев в Mnesia тоже тяжёлый (~1 MB gzip/мес × 24 × 100 ≈ 2GB disc_only). Три месяца warm ≈ **300MB** на ту же сотню.
### Дефолты (зафиксировано)
- **Hot:** все события с `start_time` в **текущем календарном месяце** (TZ календаря / UTC — в реализации, один канон) **или в будущем**.
- **Снимок месяца M:** когда наступило **7 суток** следующего месяца (`ARCHIVE_GRACE_DAYS=7`) — хвост week/month-grid и правки конца месяца; затем M **read-only**.
- **Warm:** последние **3 закрытых месяца** blob в Mnesia (`ARCHIVE_WARM_MONTHS=3`).
- **Cold:** старше 3 закрытых месяцев, **без TTL**.
- `GET …/events?from&to` **склеивает ярусы**: viewport августа 1-го числа читает хвост июля из warm/cold + август из hot.
- Recurring **master** с вхождениями в hot/будущем остаётся в hot; в snapshot — только вхождения закрытого месяца.
- **Booking** остаётся в hot (inbox `/bookings`); в snapshot — копия занятости на момент снимка (через `event_to_json`).
- Mutate в archived month → `409`. Reopen — не в волне 1.
Env (без смены контракта): `ARCHIVE_GRACE_DAYS`, `ARCHIVE_WARM_MONTHS`.
```
Hot --(месяц закрыт + 7д grace)--> Warm snapshot --(>3 закрытых мес)--> Cold file
```
## API чтения (контракт Back#76)
Клиент **не** переключается на HTML для прошлого месяца.
- Живой диапазон: `GET /v1/calendars/:id/events?from&to` — hot по времени; если `from`
задевает закрытый месяц — **merge** warm/cold (хвост month-grid). Не отдавать весь
календарь без `from`/`to` как единственный путь live-grid.
- Прошлый месяц (и week/day, курсор в archived month): тот же path и shape JSON. Сервер сам берёт warm snapshot или cold file и отдаёт события, попадающие в `from`/`to`.
- Ответ может содержать флаг `archived: true` на уровне списка или заголовка/обёртки — чтобы Front показал бейдж «Архив». Точная форма — в реализации, без второго URL.
- `GET /v1/calendars/:id/view` **нет**: HTML-календарь снят.
ACL: как у живого календаря — owner **или** share `read|write|admin` (кто уже `can_access`). Не только owner. Guest / public commercial: только hot public events; deep archive чужой студии — **не** в волне 1.
## Запись
- Create/update/delete событий и booking — только hot.
- Scheduler (один узел, как `infra_cleanup`): закрыть месяц → собрать JSON → gzip → `month_snapshot` → удалить из hot строки этого месяца (кроме защищённых master). Затем по TTL — сбросить blob в файл, ужать/убрать запись Mnesia до pointer.
- Идемпотентность: повторный прогон того же `{calendar_id, YYYY-MM}` не дублирует и не теряет данные.
- Backup (DevOps): volume `eventhub-data` включает `UPLOAD_DIR/archive/`.
## Front (контракт Front#73)
- Owner/share, вид месяц/неделя/день, **прошедший** период: React-grid по JSON, не iframe.
- Бейдж «Архив».
- Lens overview на прошлом месяце — минимум (не «нет lens, потому что HTML»).
- Текущий и будущий месяц без регрессии.
- Time Arc / ghosts на истории — **не** в волне 1.
## Non-goals волны 1
- Write в warm/cold, reopen месяца.
- Гостевой архив чужой студии.
- Time Arc на прошлых месяцах.
- S3 / внешняя БД / extra Erlang node.
- Удаление истории по TTL (файлы храним).
- Починка `archive_controller`/`slave` как целевой путь.
## Критерии приёмки канона
- [x] Этот документ согласован (Spec#20).
- [ ] Back#76: scheduler + snapshot + JSON read + тесты; peer/slave не в hot path.
- [ ] Front#73: past period на React; e2e навигации на прошлый месяц.
- [ ] После тестов: BackSpec/FrontSpec сверены с кодом (не с этим черновиком API-флага).