Зачем: ревью владельца по PR #213. Сверка конфигурации в stand.md приехала из README одним абзацем на двадцать строк, а Superset был приклеен к Airflow без пустой строки. Три места в world.md читались калькой с английского. Что: ледгер «Что проверено» разбит на записи по темам — ClickHouse, Kafka, метрики и Grafana, Airflow, Superset. В world.md заголовок «Как позвать генератор» стал «Как запустить генератор» (якоря в README и generator/README.md обновлены), переписаны абзацы про значения по умолчанию и про сокет Docker. Смысл не менялся. Словарь проекта («пульт мира», «опись мира», «работник», «сырье») не трогался: это термины CONTEXT.md и ADR, их пересмотр — отдельная развилка. Проверка: скрипт ссылок из тикета — пустой вывод; make config-test — ЗЕЛЁНО; старый якорь «как-позвать-генератор» в репозитории не встречается. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014QBCtU38qSHg5DvgtnKXcW
13 KiB
Мир и его пульт
Модельное время стенда со стороны оператора: как рождается мир, что двигает его ось, кто играет дни и где хранится позиция. Доводы об устройстве пульта — в ADR 0009, о безопасном повторном подъеме и жизненном цикле мира — в ADR 0013. Почему последний день стартового мира неполон и что хранит опись, разобрано в справочнике стартового мира и справочнике описи.
Стартовый мир
Стенд поднимается не пустым: make up через REST API запускает даг Airflow
world_initialize. Тот создаёт прикладные топики и схему, собирает образ
генератора и играет в hits первые восемь дней модельного времени — понедельник
по понедельник, 401 185 событий. Следом второй запуск генератора отправляет в
orders семь слепков — за дни 0…6. Даг ждёт приёма обоих источников и только
тогда ставит позицию мира в 8. Так у всякой лабы есть данные, и всегда одни и
те же.
Последний день намеренно неполон: события дня 7 уже приехали, а его слепок уедет только с первым ходом мира. Поэтому на свежем стенде правый край графика выручки отстаёт от кликстрима и дозаполняется по мере движения мира — так виден разный темп потокового трекера и ночной выгрузки магазина.
Повторный make up безопасен: world_initialize видит готовую позицию,
сообщает в журнале, что мир уже существует, и не применяет DDL, не собирает
образ и не заливает данные повторно. Изменённую схему применяют адресно либо
явной командой make rebuild-storage.
Опись мира
Сам мир в git не хранится — он чистая функция зерна, и держать его в
репозитории значило бы держать там кэш. Вместо него лежит опись мира,
data/world-inventory.json: зерно, версия
генератора, хеш каталога товаров и по строке на каждый день — дата, число
событий и хеш его байтов. У дней с закрытым окном там же лежат счётчики
классов расхождений по заказам. Опись отвечает на единственный вопрос: тот ли
это мир, что был вчера.
Спрашивают её двое. make test в generator/ сверяет опись с тем, что
собирается из кода сегодня: правка генератора меняет мир, и опись надо
пересобрать — make inventory там же. make check-clickhouse в корне сверяет
с описью то, что доехало до ods.event, подневно. Хеш дня можно пересчитать и
руками — это обычный sha256sum файла, который пишет файловый приёмник:
uv run --project generator python -m clickstream_generator batch \
--day 0 --file tmp/day0.jsonl
sha256sum tmp/day0.jsonl
Как запустить генератор
События производит генератор из generator/. Модельный день —
функция зерна и номера дня, поэтому один и тот же день всегда даёт те же
события: обрыв лечится повтором. Позицию на оси генератор не помнит — какой
день играть, решает зовущий.
Режима два, и различаются они темпом. Пакетный гонит день подряд, без пауз: так заливается мир и так переигрывается день после обрыва. Живой держит темп модельного времени — по умолчанию ×60: модельные сутки за 24 реальные минуты, суточная волна разворачивается на глазах, отставание видно в логе.
На стенде генератор ходит своим образом — разовой службой, которую поднимают и убирают на один прогон. На каждый режим по цели:
# день D0 целиком в топик hits
make generate-batch
# первая сотня событий дня D3
make generate-batch GENERATOR_DAY=3 GENERATOR_LIMIT=100
# день D3 живьём, ускорение ×1000
make generate-live GENERATOR_DAY=3 GENERATOR_SPEED=1000
| Переменная | Что задаёт | Умолчание |
|---|---|---|
GENERATOR_DAY |
номер дня на оси мира (D0 — первый) | 0, задано в Makefile |
GENERATOR_LIMIT |
потолок событий на прогон; только generate-batch |
нет: день целиком |
GENERATOR_SPEED |
ускорение модельного времени; только generate-live |
×60, задано в генераторе |
Куда отправлять, обеим целям задаёт сам стенд: внутри сети Compose это брокер
kafka:9092 и топик hits.
Образ цели не пересобирают: нет образа — Compose соберёт его сам, есть —
возьмёт как есть. Пересобрать намеренно, после правки generator/Dockerfile
или зависимостей, — отдельной командой:
docker compose --profile generator build generator
Тот же образ несёт заливка стартового мира. Перед такой заливкой
world_initialize сам собирает его из текущего дерева; на готовом мире
повторный make up до сборки не доходит.
Разведено это нарочно: собрать образ и запустить контейнер — разные действия, и цель запуска, молча пересобирающая образ, стирает между ними границу. Что образ устарел, видно по собственному прогону — это обратная связь, а не ловушка.
Посмотреть на события, не поднимая стенд, помогает файловый приёмник: одно
событие — одна строка. Флаг --limit берёт начало дня вместо целого дня —
тому, кто смотрит на конвейер, ждать полсотни тысяч событий незачем.
uv run --project generator python -m clickstream_generator batch \
--day 0 --limit 100 --file tmp/day0.jsonl
Несколько дней подряд играет один запуск: --days 8. Всё, что принимается
флагами, принимается и переменными окружения — так генератор позовёт даг
этапа 5; полный список у --help.
Значения по умолчанию есть не у всех параметров, и это сделано специально.
Зерно, число дней и темп живого дня описывают модель мира — их генератор знает
сам. День на оси, приемник и имя топика описывают стенд, на котором генератор
запустили: если их не задать, он откажется работать и вернет ненулевой код еще
до первого события. Поэтому значение дня по умолчанию живет в Makefile: день
задает тот, кто запускает, а не тот, кого запускают.
Пульт мира
Считать дни самому необязательно. В Airflow живёт пульт мира: два дага создают его начальное состояние, а три двигают ось дальше.
| Даг | Что делает |
|---|---|
world_initialize |
создаёт стартовый мир, если его ещё нет; повторный запуск ничего не меняет |
world_recreate |
удаляет только прикладной мир и запускает его создание заново |
world_next_day |
играет следующие дни пачкой; сколько — параметр запуска, по умолчанию один |
world_live_day |
играет следующий день в темпе модельного времени, около двадцати четырёх минут |
world_live |
выключатель: пока снят с паузы, дёргает world_live_day день за днём |
Работник не только играет день: следом он отправляет слепок заказов за
сыгранное и дёргает отдельный даг, orders_ingest, — тот забирает приехавший
слепок из топика orders в сырьё, — и дожидается его конца. Так две половины
стенда идут в ногу: упал приём — краснеет и работник, а позиция остаётся на
месте (ADR 0008).
Позицию хранит переменная Airflow world_position — номер первого несыгранного
дня. Ставит её работник, сыгравший день, и только по успеху: оборванный прогон
позицию не двигает, и следующий запуск играет тот же день заново. Отсутствующая
переменная означает, что мир ещё не создавали; -1 — очистка не закончена,
0 — очистка закончена, а начальная генерация ещё не завершена. Работники
начинают играть только с позиции 8 и больше.
В форме запуска можно выбрать только число дней. Первый день работник берет из
world_position. Поля «Logical date» и «Run ID» спрятаны в «Advanced Options».
Они нужны самому Airflow и на выбор дня не влияют. Сыгранные дни и новую
позицию работник пишет в журнал шага remember_played.
Выключатель создаётся на паузе. Снимите — мир поедет сам; поставите обратно — встанет на границе модельных суток, доиграв начатый день. Форма пульта и доводы целиком — ADR 0009.
У этого решения есть цена: планировщику Airflow отдан сокет Docker —
иначе ему не поднять контейнер генератора. Доступ к сокету равен правам root
на машине; для локального учебного стенда это приемлемо, но знать об этом
нужно. Доступ дает не само монтирование сокета, а членство в группе docker:
у нее на каждой машине свой GID, он записан в .env, а узнать его можно
командой getent group docker.