Files
clickstream-ch-kafka-supers…/.scratch/generator-model-time-startup-history/hitl-findings.md
T
ddadminandClaude Opus 4.8 91d15e60a0 docs(generator): находки ручного HITL пути менти и handoff
- Зачем:
  - долг ручной HITL-проверки пути менти переносился пятый handoff подряд;
    проверка вскрыла набор находок и наметила редизайн пути менти.
- Что:
  - добавлены находки HITL (F1-F9) с рамочной моделью трёх режимов менти:
    import-база, ветки next-day и continue, под каждую — свой урок/лаба.
  - зафиксированы решения: 3-дневный артефакт на daily-wave, сжатие xz,
    один учебный профиль, беспараметрный next-day, инкрементальные счётчики.
  - добавлен handoff по ADR-0003 с планом раскладки находок в постановки.
- Проверка:
  - чтение .scratch/generator-model-time-startup-history/hitl-findings.md
    и .scratch/handoffs/20260719-2144-hitl-mentee-path-redesign.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 21:52:12 +03:00

20 KiB
Raw Blame History

Находки ручной 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-solutionxz -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 дней).