Files

8.5 KiB
Raw Permalink Blame History

Архив календаря (Фаза 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 удалены: история только через 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 как целевой путь.

Критерии приёмки канона

  • Этот документ согласован (Spec#20).
  • Back#76: scheduler + snapshot + JSON read + тесты; peer/slave не в hot path.
  • Front#73: past period на React; e2e навигации на прошлый месяц.
  • После тестов: BackSpec/FrontSpec сверены с кодом (не с этим черновиком API-флага).