From 91d15e60a01a7ea12ea2703f079489626ba6929f Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 19 Jul 2026 21:52:12 +0300 Subject: [PATCH] =?UTF-8?q?docs(generator):=20=D0=BD=D0=B0=D1=85=D0=BE?= =?UTF-8?q?=D0=B4=D0=BA=D0=B8=20=D1=80=D1=83=D1=87=D0=BD=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20HITL=20=D0=BF=D1=83=D1=82=D0=B8=20=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=20=D0=B8=20handoff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - долг ручной 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 --- .../hitl-findings.md | 236 ++++++++++++++++++ ...20260719-2144-hitl-mentee-path-redesign.md | 103 ++++++++ 2 files changed, 339 insertions(+) create mode 100644 .scratch/generator-model-time-startup-history/hitl-findings.md create mode 100644 .scratch/handoffs/20260719-2144-hitl-mentee-path-redesign.md diff --git a/.scratch/generator-model-time-startup-history/hitl-findings.md b/.scratch/generator-model-time-startup-history/hitl-findings.md new file mode 100644 index 0000000..17373d6 --- /dev/null +++ b/.scratch/generator-model-time-startup-history/hitl-findings.md @@ -0,0 +1,236 @@ +# Находки ручной 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 дней). diff --git a/.scratch/handoffs/20260719-2144-hitl-mentee-path-redesign.md b/.scratch/handoffs/20260719-2144-hitl-mentee-path-redesign.md new file mode 100644 index 0000000..b9d3ac5 --- /dev/null +++ b/.scratch/handoffs/20260719-2144-hitl-mentee-path-redesign.md @@ -0,0 +1,103 @@ +# Handoff: ручной HITL пути менти пройден — вырос в набросок редизайна + +Дата: 2026-07-19 21:44. +Жанр: handoff по ADR-0003 после сессии «ручная HITL-проверка пути менти» +(долг переносился пятый handoff подряд — закрыт). + +## Что сделано + +- **HITL-проверка пути менти пройдена целиком, своими руками через UI.** + Чистый стенд (`make clean` + `make up`) -> Airflow UI: `ddl_init` -> + `generator_control` `backfill` (профиль `ci`) -> ETL -> `make superset-init` + -> дашборд глазами -> `generator_control` `next-day` -> проверка стыка. + Каждый шаг сверен по данным (ClickHouse, manifest, chain-check). +- **Результат мира:** backfill `[00:00, 06:00)` = 16 054 события; после + `next-day` = 110 602 события, визитов 10 228, пользователей 1 726; + `boundaries` = три границы, `model_t_end` = `2026-01-02 06:00`. + `generated-history-chain-check` — EXIT 0 (стык 06:00: 9 переходящих + визитов, непарных 0, однородность подтверждена). +- **9 находок + рамочная модель** — в + `.scratch/generator-model-time-startup-history/hitl-findings.md`. + Половина находок родилась из наблюдений менти. HITL из «проверить, что + не сломалось» перерос в **набросок редизайна пути менти**. + +## Ключевые выводы (кратко; подробно — в hitl-findings.md) + +- **Рамочная модель менти:** основной режим — `import` артефакта (база), + дальше две ветки от одного состояния: `next-day` (пакетно) и `continue` + (живой поток). Это три разных лабораторных. `next-day` и `continue` + не схлопывать. Подтверждено `launch.py:117-137`. +- **F7 + замер:** опыт на 6h беден; нужен готовый артефакт из файла. + Артефакт 6h = 49 МБ; `xz -9e` -> 1,1 МБ (48x, 13,5 с). На 3 дня ~13 МБ + — кладётся в обычный git. Паттерн-образец: `airflow-greenplum-solution` + (`bookings/seed/demo.sql.xz`). +- **Решение менти:** целевой стартовый мир — **3 дня на `daily-wave`** + (обсуждаемо). На `ci` три дня плоские — волну даёт `daily-wave`. +- **F8:** два профиля путают. Свести к одному учебному (волна, ×60); + `ci` (×1) — служебный для тестов. ×60 оправдан live-лабой (`continue`). +- **F9:** `next-day` растёт O(N²) из-за полной перечитки Kafka для + счётчиков (`service.py:591`). Параллелить не то (ломает детерминизм); + правильно — инкрементальные счётчики (O(N), снимает риск OOM). Это и + есть решение «новый формат состояния manifest», блокирующее расписание. +- **F1, F5 (баги UI):** форма `generator_control` держит необязательные + поля обязательными (`type="string"` без `null`); Airflow и Superset + быстро разлогинивают (ротацию секрета исключил, корень TBD). + +## Состояние + +- Ветка `feature/data-generator`. После этого handoff в дереве останутся: + `hitl-findings.md` + этот handoff (коммитятся), плюс **не коммитить** + `data/ci_backfill.json` (49 МБ, побочный от backfill; `.gitignore` его + не покрывает — риск, см. ниже). +- Стенд **оставлен поднятым** по просьбе пользователя; в мире 1,25 суток + (профиль `ci`), live-генератор не запущен. Выросший мир можно смотреть + в Superset (dashboard id 1). + +## Что дальше + +1. **Разложить находки в постановки по плейбуку** (этапы 1-2, двойное + слепое ревью). Ориентировочно ~4 задачи (номера 21+ в `issues/`): + - **Ядро:** эталонный 3-дневный артефакт (`daily-wave`) как база + менти через `import`; формат/сжатие (`xz`, нужен ли `raw_topics`); + место хранения (git после отладки сида). Снимает F7. + - **Профиль:** один учебный профиль, `ci` -> служебный тестовый. + Перед слиянием проверить, не зависит ли live-тест от скорости ×1 + (F8). + - **Расписание + retention:** беспараметрный DAG `next-day` (F1/F3), + `schedule`, инкрементальные счётчики manifest (F9/F6). Это одна + связка — их корень общий. + - **Баг-фиксы:** F1 (форма) и F5 (разлогин); F4 (JS-баг Grid) низкий. +2. **Три режима -> свои уроки/лабы** — вход в учебный контент + (`docs/course/`). Под каждую ветку отдельная лаба: `next-day` + (пакетная инкрементальная обработка) и `continue` (потоковый приём); + общая база `import` — их совместное начало. Отдельно от + инфраструктурных задач. +3. **Мелочь по гигиене:** `data/*.json` не покрыт `.gitignore` — + побочные артефакты backfill (49 МБ) рискуют попасть в коммит. Решить: + правило в `.gitignore` или чистить артефакты после прогонов. + +## Проверки этой сессии + +- `ddl_init`: 4 слоя созданы (stg 12 / ods 8 / dds 2 / dm 6 таблиц). +- Форма мира после backfill: пирамида 445 < 1516 < 16 054, интервал + `[00:00, 06:00)` точный. +- После `next-day`: 3 границы в manifest, мир до `2026-01-02 06:00`. +- `generated-history-chain-check`: EXIT 0. +- Длительность `next-day` (метаданные Airflow): задача `run_next_day` + 638 с (~10,6 мин); ETL `trigger_etl` 30 с — узкое место в генерации. +- Замер сжатия: `xz -9e` 48x за 13,5 с; gzip -9 17x. + +## Отклонения процесса + +- Ревью субагентами не запускали: пользователь просил «свежий взгляд», + что по договорённости = без субагентов. Проверка проведена мной руками. +- Обход бага F1: форму backfill/next-day заполняли значениями профиля + `ci` целиком (пустые необязательные поля UI не пропускает). + +## Suggested skills + +- `claude-subagent-playbook` — разложить находки в постановки тем же + конвейером (этапы 1-2 с нуля). +- `conventional-commits` — коммиты. +- `research` / Context7 — для задачи про профиль/форму (Airflow Param + `type=["null","string"]`) и про формат артефакта.