Зачем: README был складом сведений, осевших там, где писались. По раскладке резолюции #211 он становится лицом репозитория: промо-блок, «куда я попал», типовые действия, ссылки. Слово «менти» уходит из README и путеводителя, хвосты #212 и #214 закрываются. Что: промо-цитата со ссылкой на план обучения и ментора; шапка с нейтральной адресацией и одной фразой о составе и пути данных; «Быстрый старт» сжат до двух команд по решению владельца — проверки остаются роботам, в README одна ссылка на карту проверок; разделы «Адреса и пароли», «Сдвинуть мир вперед», «Вернуть мир к началу», «Выключить или сбросить целиком», «Подключиться из DBeaver»; «Документация» называет все семь справочников вложенным списком. Умерли «Статус», «Что здесь будет», «Чем отличается от предшественника» и три заголовка-якоря. В world.md снята устаревшая фраза о «даге этапа 5», stand.md и ADR 0007 ведут на новый раздел «Адреса и пароли», в путеводителе снято слово «менти». Проверка: скрипт ссылок пуст, grep «менти» по README и путеводителю пуст, make config-test зеленый. Саморевью Sol 1/1; слепое ревью Opus — 0 блокеров, 2 minor, 3 nit, все закрыты; вычитка прозы ai-text-lint и оптикой литературного русского — 15 находок, 9 внесены, 6 отклонены как слова владельца и термины. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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. Всё, что
принимается флагами, принимается и переменными окружения — так генератор
запускают даги модельного времени; полный список у --help.
Значения по умолчанию есть не у всех параметров, и это сделано специально.
Зерно, число дней и темп живого дня описывают модель мира — их генератор знает
сам. День на оси, приёмник и имя топика относятся к конкретному запуску
генератора на стенде: если их не задать, он откажется работать и вернёт
ненулевой код ещё до первого события. Поэтому значение дня по умолчанию живёт в
Makefile: день задаёт тот, кто запускает, а не тот, кого запускают.
Даги модельного времени
Считать дни самому необязательно. В Airflow живут даги модельного времени: два дага создают начальное состояние мира, а три двигают ось дальше.
| Даг | Что делает |
|---|---|
world_initialize |
создаёт стартовый мир, если его ещё нет; повторный запуск ничего не меняет |
world_recreate |
удаляет только прикладной мир и запускает его создание заново |
world_next_day |
играет следующие дни пачкой; сколько — параметр запуска, по умолчанию один |
world_live_day |
играет следующий день в темпе модельного времени, около двадцати четырёх минут |
world_live |
выключатель: пока снят с паузы, запускает world_live_day день за днём |
Сыграв день, шаговый даг отправляет слепок заказов и запускает даг
orders_ingest. Тот забирает слепок из топика orders в stg; шаговый даг
ждёт его завершения. Так две половины стенда идут в ногу: упал приём — падает и
шаговый даг, а позиция остаётся на месте (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.