14da77fd7e
Fixes EventHub/EventHubSpec#20 Co-authored-by: Cursor <cursoragent@cursor.com>
116 lines
8.5 KiB
Markdown
116 lines
8.5 KiB
Markdown
# Архив календаря (Фаза 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 | 7–14 дней |
|
||
| 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 день) | ~1100–2000 |
|
||
| закрытый месяц после grace | **0** исторических месяцев |
|
||
|
||
100 таких студий: 90д lookback ≈ **0.5M** лишних hot-строк в `disc_copies` на **каждый** узел кластера. Warm-blob 24 месяцев в Mnesia тоже тяжёлый (~1 MB gzip/мес × 24 × 100 ≈ 2 GB disc_only). Три месяца warm ≈ **300 MB** на ту же сотню.
|
||
|
||
### Дефолты (зафиксировано)
|
||
|
||
- **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-флага).
|