- Зачем:
- находки ручного 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.
20 KiB
Находки ручной 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(см. постановку про расписание). Отдельный беспараметрный DAGnext-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, SupersetPERMANENT_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_day15 с,trigger_etl(ETL full-refresh по 110k событий) 30 с,check_after_etl6 с. Узкое место — именно генерация плюс накопительный пересчёт внутриrun_next_day, не ETL. - Три причины:
- модельный день = 24 ч против 6 ч у backfill -> ~вчетверо больше событий за прогон;
- генерация — однопоточный Python, CPU-bound: много ядер не помогают, упор в скорость одного ядра;
- задокументированный накопительный пересчёт по всей истории Kafka растёт с числом дней (на дне 2 мал, но копится).
- Связка с задачей про расписание: это ровно та причина, по которой
перед включением
scheduleнужно решить retention/формат накопительного состояния (иначе плановый ежедневныйnext-day— растущий многоминутный CPU-burn). Менти пощупал стоимость руками. См. F3.
F7. Опыт менти беден на 6h; «история из файла» упирается в размер (DESIGN)
- Мысль менти: 6 часов backfill — мало для опыта; хотели хранить больше и не тратить время менти на генерацию, а грузить из файла.
- Состояние: механизм есть — глагол
importи runbookdocs/runbooks/startup-history.md(задача 07done). Но готового артефакта в репозитории нет: менти всё равно либо генерирует (медленно, см. F6), либо ищет «где взять файл». - Замер: артефакт за 6 ч = 49 МБ (плоский JSON). Экстраполяция: день ~200 МБ, неделя ~1,4 ГБ, месяц ~6 ГБ. В git такое не кладут.
- Причина раздутости: артефакт содержит и
state, иraw_topics— похоже, тащит сырые сообщения Kafka целиком (~8 МБ на модельный час). - Развилка (пересекается с F6/расписанием):
- целевой размер демо-мира (день/неделя?);
- формат/сжатие артефакта (
xz— см. замер ниже; нужен лиimport-уraw_topics, или хватит компактногоstate); - где хранить раз не 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).
- Три куска стоимости:
- Генерация нового дня (строки 545–583) — O(события дня), постоянна, не растёт.
- Полная перечитка Kafka (строка 591,
KafkaDataTopicReader.load()+ManifestCounters.add_batch) — читает все data-топики с начала в RAM, пересчитывает счётчики/суммы с нуля. Растёт с историей. - 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 дней).