Files
clickstream-data-platform/docs/architecture/world.md
T
ddadminandClaude Fable 5.1 bad5981904 docs(architecture): операторские разделы README переехали в справочники
Зачем: README служил складом для двух зон без своих справочников —
«службы и доступ» и «мир и его пульт». Раскладка принята резолюцией
#211: README остается лицом, устройство живет в docs/architecture/.

Что: новые docs/architecture/stand.md («Стенд: службы, доступ,
настройка», доводы — ADR 0001 и 0007) и docs/architecture/world.md
(«Мир и его пульт», доводы — ADR 0009 и 0013). Доводы смоука и
описания целей make слиты в testing.md без повторов: фраза о
check_containers_survived и абзац про config-test умерли как дубли.
На месте каждого переехавшего раздела README — абзац со ссылкой,
адреса и пароли интерфейсов остались. Путеводитель и README
генератора ссылаются на world.md вместо якорей README. Переехавший
текст не правился, только относительные ссылки.

Проверка: скрипт ссылок из тикета — пустой вывод; make config-test —
ЗЕЛЁНО; подиффовая сверка переехавших диапазонов с README@2588515 —
без расхождений (слепое ревью); в новой прозе нет «ё».

Closes #212

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QBCtU38qSHg5DvgtnKXcW
2026-09-01 22:49:23 +03:00

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 отдан сокет докера — иначе контейнер генератора ему не поднять. Доступ к сокету равен праву root на машине; для локального учебного стенда размен принят, но знать о нём надо. Открывает дверь не монтирование, а членство в группе: GID группы docker у каждой машины свой, живёт в .env и подсматривается командой getent group docker.