Files
ddadmin d6e7a03032 docs(specs): спека редизайна пути менти — мир из артефакта и две ветки
- Зачем:
  - находки ручного HITL 2026-07-19 требовали проектного решения: путь
    менти через backfill медленный, бедный и путаный; нужна база import
    эталонного мира и две ветки роста.
- Что:
  - спека docs/specs/2026-07-19-mentee-path-redesign.md: целевая модель
    (import + next-day + continue), эталонный 3-дневный мир в git (xz),
    переименование пульта в world_init с дефолтом import, отдельный
    world_next_day, инкрементальные счётчики manifest, один учебный
    профиль; форма работ — 4 дочерних issue.
  - CONTEXT.md: термины «мир (стенда)», «эталонный мир», «три режима
    менти».
  - .scratch/hitl-findings.md восстановлен из среза 0e312b3 как рабочий
    материал фичи (до разбора в issues).
- Проверка:
  - вычитка против hitl-findings и решений обсуждения 2026-07-19.
2026-07-19 23:18:17 +03:00

237 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
# Находки ручной HITL-проверки пути менти
Сессия 2026-07-19. Проверяем путь менти своими глазами: Airflow UI ->
Superset -> Kafka UI. Стенд чистый (`make clean` + `make up`), профиль `ci`.
## Рамочная модель: три режима менти (подтверждено, launch.py:117-137)
Основной режим менти — **загрузка из архива**, дальше две ветки от одного
восстановленного состояния (`GEN_STATE_RESET=false`). Это и есть каркас,
в который ложатся все находки ниже. Из каждой ветки можно сделать отдельную
лабораторную.
- **База: `import`** артефакта (целевой размер 3 дня, `daily-wave`) ->
мир на `T_end`. Общий фундамент обеих веток. См. F7, F8.
- **Ветка A: `next-day`** (`GEN_RUN_MODE=next-day`) — пакетно добавить
сутки. Лаба «инкрементальная обработка»: инкремент vs full-refresh,
чистота стыков, расписание. Сюда бьют F1, F3, F6, F9.
- **Ветка B: `continue`** (`GEN_RUN_MODE=live`) — непрерывный живой поток
от той же границы. Лаба «потоковый приём»: near-real-time ETL,
мониторинг Grafana, стык backfill/live. **Требует ×60** — оправдывает
скорость учебного профиля (уточнение к F8).
Важно: `next-day` и `continue` — разные педагогики, не схлопывать.
**Под каждую ветку — свой урок/лаба** (учебный контент, `docs/course/`):
одна про пакетную инкрементальную обработку (`next-day`), другая про
потоковый приём (`continue`). Общая база `import` — их совместное начало.
`backfill`/`reset` (`STATE_RESET=true`, «с нуля»): в **целевой** модели
`backfill` — инструмент мейнтейнера для сборки артефакта, а не первый шаг
менти. Сегодня ещё наоборот — backfill остаётся каноническим первым
прогоном менти (см. F1/F2); этот сдвиг и есть суть редизайна.
## F1. Форма `generator_control` помечает необязательные параметры обязательными (BUG)
- **Где:** Airflow UI -> `generator_control` -> Trigger DAG w/ config.
- **Симптом:** все поля формы (`duration`, `seed`, `model_time_speed`,
`artifact_path`, `expected_t_end`) показаны с красной `*` и обязательны.
Браузерная валидация `required` не даёт отправить форму с пустым полем.
Споткнулись первым на пустом `duration`.
- **Причина:** в `dags/generator_control_dag.py` эти `Param(...)` объявлены
с `type="string"` без `"null"`. Airflow для типа без `null` вешает на input
HTML-атрибут `required`.
- **Противоречие с документами:** `docs/OPERATIONS.md` и описания самих
Param говорят «пусто — взять из профиля / не сохранять». То есть поля
задуманы необязательными, но UI это запрещает.
- **Влияние на менти:** канонический первый прогон «backfill с профилем,
остальное пусто» через UI невозможен без обходного заполнения.
- **Кандидаты решения:** сменить тип необязательных Param на
`type=["null","string"]` (идиома Airflow для необязательной строки) —
проверить актуальность через Context7; либо, как минимум, поправить
формулировку в runbook. Предпочтителен первый.
- **Обход в этой сессии:** заполнили все поля значениями профиля `ci`
(`duration=6h`, `GEN_SEED=4242`, `GEN_MODEL_TIME_SPEED=1`,
`artifact_path=/opt/airflow/data/ci_backfill.json`,
`expected_t_end=2026-01-01T06:00:00+00:00` — для backfill игнорируется).
## F2. Superset: гео-карта заменена столбцами — ПОДТВЕРЖДЕНИЕ, не дефект
- **Наблюдение менти:** на дашборде вместо гео-карты — столбчатый
«Top Countries by Events». На первый взгляд неожиданно.
- **Проверка:** это осознанное решение задачи 10 (`done`). Legacy-виз
`world_map` (choropleth) не даёт настроить tooltip/легенду/шкалу, а
гео-распределение сильно перекошено (US-доминанта из статического сида
`geo_by_click_id`, своя генерация гео — отдельный шаг по ADR-0006).
Топ-N стран столбцами читается лучше карты. Критерии приёмки задачи 10
это фиксируют.
- **Вывод:** в UI задача 10 приземлилась корректно. Дефекта нет.
## Остальной дашборд (backfill-путь, профиль ci)
Всё сходится с данными и здорово:
- KPI 16 054 событий / 445 пользователей / Avg 10.6 / Conversion 7.1%.
- Events over Time ровный за `[00:00, 06:00)`; воронка монотонно убывает.
- «Rows by Layer» — 4 ровных столбика ~16k: события не теряются на
переходах STG->ODS->DDS->DM (наглядный контроль целостности).
- Связанные фильтры между визами работают (проверил менти глазами).
## F3. Пульт `generator_control` неудобен для менти и не годится в расписание (DESIGN)
- **Наблюдение менти:** `next-day` спрятан в универсальном DAG с дефолтом
`backfill` и тяжёлой формой; `expected_t_end` для менти — лишнее
усложнение (на автосхеме мира границу знать неоткуда).
- **Предложение менти:** сделать `next-day` отдельным DAG, который
«достаточно триггернуть, ничего не нажимая лишнего».
- **Почему важно:** это прямой вход в задачу про расписание. У планового
запуска не должно быть формы с обязательными полями и дефолта `backfill`
(см. постановку про расписание). Отдельный беспараметрный DAG `next-day`
решает и удобство менти, и пригодность к `schedule`.
- **Связка:** усиливает F1 (форма требует необязательные поля) — общий
корень в том, что один DAG обслуживает и ручной backfill, и то, что
хочется автоматизировать.
## F4. Airflow Grid: Auto-refresh Error (JS)
- **Симптом:** всплывающая ошибка `can't access property "find",
p.dagRuns is undefined` при авто-обновлении Grid во время запуска.
- **Оценка:** похоже на известный косметический баг UI Grid (гонка
авто-refresh, пока у DAG ещё нет прогонов). Работе не мешал. Проверить
версию Airflow и известные issue; при подтверждении — низкий приоритет.
## F5. Быстрый разлогин в Airflow И Superset (BUG, корень TBD)
- **Симптом:** обе веб-морды стремительно разлогинивают в рамках сессии.
- **Что исключено:** ротация секрета. `AIRFLOW__WEBSERVER__SECRET_KEY`
фиксирован (литерал по умолчанию, `AIRFLOW_SECRET_KEY` в `.env` не задан),
`SUPERSET_SECRET_KEY` — жёстко зашитый литерал. Значит, «каждый gunicorn
worker подписывает своим ключом» — не причина.
- **Куда копать:** время жизни сессии (Airflow
`session_lifetime_minutes`, Superset `PERMANENT_SESSION_LIFETIME` — в
`configs/superset` не задан), настройки cookie (SameSite/Secure на
localhost с разными портами), окружение браузера.
- **Влияние на менти:** сильно портит опыт — заставляет постоянно
перелогиниваться. Кандидат в отдельную задачу.
## F6. `next-day` собирается долго (~10,6 мин на дне 2), CPU-bound (PERF/DESIGN)
- **Наблюдение менти:** «как долго собирается следующий день… и это на
мощном процессоре».
- **Замер по метаданным Airflow (этот прогон):** задача `run_next_day` =
**638 с (~10,6 мин)** (18:03:26 -> 18:14:04). Соседние задачи мелкие:
`precheck_next_day` 15 с, `trigger_etl` (ETL full-refresh по 110k
событий) 30 с, `check_after_etl` 6 с. Узкое место — именно генерация
плюс накопительный пересчёт внутри `run_next_day`, не ETL.
- **Три причины:**
1. модельный день = 24 ч против 6 ч у backfill -> ~вчетверо больше
событий за прогон;
2. генерация — однопоточный Python, CPU-bound: много ядер не помогают,
упор в скорость одного ядра;
3. задокументированный накопительный пересчёт по всей истории Kafka
растёт с числом дней (на дне 2 мал, но копится).
- **Связка с задачей про расписание:** это ровно та причина, по которой
перед включением `schedule` нужно решить retention/формат накопительного
состояния (иначе плановый ежедневный `next-day` — растущий многоминутный
CPU-burn). Менти пощупал стоимость руками. См. F3.
## F7. Опыт менти беден на 6h; «история из файла» упирается в размер (DESIGN)
- **Мысль менти:** 6 часов backfill — мало для опыта; хотели хранить
больше и не тратить время менти на генерацию, а грузить из файла.
- **Состояние:** механизм есть — глагол `import` и runbook
`docs/runbooks/startup-history.md` (задача 07 `done`). Но готового
артефакта в репозитории нет: менти всё равно либо генерирует (медленно,
см. F6), либо ищет «где взять файл».
- **Замер:** артефакт за 6 ч = **49 МБ** (плоский JSON). Экстраполяция:
день ~200 МБ, неделя ~1,4 ГБ, месяц ~6 ГБ. В git такое не кладут.
- **Причина раздутости:** артефакт содержит и `state`, и `raw_topics` —
похоже, тащит сырые сообщения Kafka целиком (~8 МБ на модельный час).
- **Развилка (пересекается с F6/расписанием):**
1. целевой размер демо-мира (день/неделя?);
2. формат/сжатие артефакта (`xz` — см. замер ниже; нужен ли `import`-у
`raw_topics`, или хватит компактного `state`);
3. где хранить раз не git (git-lfs / релизный ассет / внешнее хранилище /
«сгенерировать один раз и закэшировать локально»).
- **Вывод:** «богатый мир из файла» и «растить мир расписанием» — один
общий вопрос: как дёшево хранить и переносить много истории. Решать
вместе. Runbook про размер/хранение сейчас молчит — дополнить.
### Решение по целевому размеру (менти, 2026-07-19, обсуждаемо)
- **Целевой стартовый размер демо-мира — 3 модельных дня.** «Больше
одного, но не слишком далеко». Срок обсуждаем.
- **Почему 3:** периодичность видна от 2 дней; 3 дают чёткий паттерн +
один «средний» день без краевых эффектов; появляется сравнение
день-к-дню в Superset. Размер ~600 МБ плоского JSON, gzip ~3060 МБ.
- **Следствие про профиль:** на `ci` (ровная интенсивность, jitter=0)
три дня будут плоскими и скучными. Суточную волну даёт `daily-wave`.
Эталонный артефакт для менти собирать на `daily-wave`, а не `ci`.
Генерация 3 дней разово мейнтейнером — ок (backfill без сна, минуты),
менти только `import`.
### Замер сжатия (2026-07-19)
- Артефакт 6h: raw 49 МБ. **`xz -9e` → 1,1 МБ (48×), 13,5 с.** gzip -9 →
3,0 МБ (17×). xz почти втрое лучше.
- Пересчёт на 3 дня: raw ~590 МБ → **xz ~13 МБ** — кладётся в обычный git
без lfs.
- Паттерн-образец: `~/sources/airflow-greenplum-solution` — `xz -9` жмёт
сид один раз в `bookings/seed/demo.sql.xz` (в git), на загрузке
`xz -dc | ...` стримит. Переносим один в один: одноразовое медленное
сжатие мейнтейнером, потом простой git.
- Вывод менти: сжимать долго один раз не страшно; когда сид отладим —
артефакт кладём в обычный git.
## F8. Два профиля (`ci`/`daily-wave`) путают; менти нужен один (DESIGN)
- **Наблюдение менти:** планировался один удобный профиль с учебной
ценностью; зачем два — непонятно.
- **Что есть (`generator/src/clickstream_generator/launch.py`):**
- `ci` — 6h, скорость ×1, jitter=0. Профиль **автотестов/CI**: быстрый,
маленький, плоский (суточной волны не видно).
- `daily-wave` — 2d, скорость ×60 (сутки ~24 мин), задача 14
«суточная волна за минуты занятия». **Учебный** профиль с волной.
- **Где протекло:** форма backfill перечисляет профили по алфавиту,
первым идёт `ci` → менти по умолчанию подсовывается тестовый плоский
профиль. Сегодняшний прогон шёл на `ci`, оттого дашборд ровный.
- **Куда вести:** при `import` готового артефакта менти профиль не
выбирает вовсе — выбор профиля уходит мейнтейнеру (сборка артефакта на
учебном профиле) и CI. Технически можно оставить один базовый учебный
профиль (волна, ×60), а CI переопределяет длительность на 6h (backfill
без сна, скорость на генерацию артефакта не влияет). Тогда отдельный
`ci` менти не нужен.
- **Проверить перед слиянием:** не зависит ли live-тест (runtime-seam)
от скорости ×1. Если нет — профили честно схлопываются в один.
## F9. Стоимость `next-day`: параллелить не то, инкремент — то (PERF/DESIGN)
Разбор по коду (`generator/src/clickstream_generator/service.py:_run_next_day`).
- **Три куска стоимости:**
1. Генерация нового дня (строки 545–583) — O(события дня), постоянна,
не растёт.
2. Полная перечитка Kafka (строка 591, `KafkaDataTopicReader.load()` +
`ManifestCounters.add_batch`) — читает все data-топики с начала в
RAM, пересчитывает счётчики/суммы с нуля. **Растёт с историей.**
3. ETL `full_refresh` (отдельный DAG) — перестраивает DDS/DM по всему
миру. Тоже растёт.
- **Параллелить — неправильный рычаг:**
- Генерацию нельзя без потери детерминизма (один поток ГПСЧ,
переходящие сессии, шагающее модельное время; воспроизводимость по
seed — базовая ценность). И она не растёт.
- Перечитку можно раскидать по потокам, но это лишь постоянный
множитель. Корень — O(N²) по N дням (день k перечитывает k дней).
- **Оптимизировать — правильный рычаг:**
- Счётчики инкрементальные: хранить накопленное состояние
`ManifestCounters` в manifest/state, добавлять только новый день
(его `sent_counts` уже посчитаны при генерации). O(N²) -> O(N).
- Убирает и рост RAM: сейчас `reader.load()` держит всю историю в
памяти (3 дня ~неск. ГБ распарсенного JSON — риск OOM).
- Формат: уникальность (`click_ids`/`user_ids`) сейчас через `set` по
всей истории — перенести множества в state; контрольную сумму
сделать катящейся (комбинировать посуточные), не хэш всего заново.
- **Связка:** это и есть решение «новый формат накопительного состояния
manifest», которое runbook называет обязательным перед расписанием
(см. F6). Оптимизация = сердцевина retention-задачи, не отдельная.
- **Режимы:** разовая сборка артефакта — медленно не страшно;
плановый ежедневный `next-day` — инкремент обязателен (через месяц
каждый запуск перечитывал бы 30 дней).