# Находки ручной 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 ~30–60 МБ. - **Следствие про профиль:** на `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 дней).