Fixes EventHub/EventHubSpec#20 Co-authored-by: Cursor <cursoragent@cursor.com>
8.5 KiB
Архив календаря (Фаза 3, волна 1)
Канон хранения и чтения истории. Трекер: Spec#20, эпик Spec#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 удалены: история только через JSONGET …/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как целевой путь.
Критерии приёмки канона
- Этот документ согласован (Spec#20).
- Back#76: scheduler + snapshot + JSON read + тесты; peer/slave не в hot path.
- Front#73: past period на React; e2e навигации на прошлый месяц.
- После тестов: BackSpec/FrontSpec сверены с кодом (не с этим черновиком API-флага).