feat(stand): make up наполняет стенд стартовым миром, опись сторожит его

Зачем
Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с
ручной заливки данных. Теперь `make up` сам приводит стенд к одному и тому
же состоянию, а в git лежит то, чем это состояние проверяется.

Что
- Опись мира `data/world-inventory.json`: паспорт (зерно, версия
  генератора, хеш каталога) и по строке на каждый из восьми дней — дата,
  число событий, хеш байтов. Собирается `make inventory`, свежесть сторожит
  `test_inventory.py` — тем же способом, что свежесть описания выгрузки.
- Разовая служба `world-init` вышла из-под профиля и играет в топик восемь
  дней при каждом подъёме; зависимый у неё — `airflow-init`, иначе `--wait`
  считает успешно отработавшую службу упавшей.
- `scripts/wait-for-world.sh` — вторая половина `make up`: приём
  асинхронный, поэтому ждать надо доезда до `ods.event`, а не завершения
  заливки. Ограниченный цикл опроса, не пауза наугад.
- Девятая проверка `make check-clickhouse`: подневный счёт событий против
  описи, рамка по датам стартового мира, счёт через `FINAL`. При
  расхождении называет, где искать, — в событиях или в браке.
- Порог «день ≤ 30 с» снят из спеки генератора в обоих местах: замер дал
  1,7 с, порог был выше факта в восемнадцать раз. На его месте — замеры с
  датой. Раздел 9 спеки закрыт: открытых вопросов не осталось.
- Слова: «манифест» стал описью мира, «зерновой мир» — стартовым миром
  (решение владельца). Оба заведены в словарь CONTEXT.md.

Проверка
`make clean && make up` с нуля — 2 м 50 с, доехало ровно 401 185 событий.
`make check-clickhouse` зелёный (8 с), `make smoke` зелёный (9 с),
`make test` — 407 тестов за 71 с, `make lint`, `make typecheck`,
`make config-test` зелёные.

Что проверка умеет краснеть, снято двумя поломками: снос партиции
2026-06-03 дал диагноз «не доехали до ODS», негодная строка в сырье —
«сломан разбор». Строки опыта убраны, день переигран, счёт вернулся.
Тест свежести проверен молчаливой правкой цены в каталоге: покраснел.

Ссылка: #42
This commit is contained in:
2026-08-07 18:37:01 +03:00
parent 30a1e9e567
commit 7c9eeedc40
20 changed files with 625 additions and 132 deletions
+48 -4
View File
@@ -13,7 +13,9 @@ Prometheus, Grafana и общая база Postgres для метаданных.
кликстрима в [`generator/`](generator/) собран целиком со стороны клиента: у
него есть контракт схемы события, из которого собрано
[описание выгрузки](docs/formats/clickstream-event.md), модельный мир и
проигрыватель, отправляющий дни в Kafka или в файл.
проигрыватель, отправляющий дни в Kafka или в файл. События доезжают до
типизированного `ods.event`, а `make up` наполняет стенд стартовым миром —
первой неделей модельного времени.
## Быстрый старт
@@ -26,10 +28,12 @@ Prometheus, Grafana и общая база Postgres для метаданных.
хватает, стенд одинаково хорошо живёт на недорогом VPS.
Проверкам на поднятом стенде также нужны `curl`, `jq`, `awk`,
`grep`, `sed`, `tail`, `sleep` и `timeout`. По умолчанию должны быть свободны
`grep`, `sed`, `tail`, `sleep` и `timeout`. `jq` нужен и самому `make up`: им
читается опись мира, по которой он ждёт заливки. По умолчанию должны быть свободны
порты `23000`, `28080`, `28088`, `28123`, `28124`, `29000`, `29001`, `29090`
и `29092`. Проверкам без стенда — `make config-test`, `make lint`,
`make typecheck`, `make test` — и сборке документации `make docs` нужен `uv`.
`make typecheck`, `make test` — и сборкам `make docs` и `make inventory` нужен
`uv`.
Стенд запускается без `.env`:
@@ -70,6 +74,7 @@ make smoke
`superset-init` завершаются с кодом 0. Первый обновляет схему Airflow,
подготавливает администратора и подключение к `clickhouse-01`. Второй обновляет
Superset, создаёт администратора и импортирует подключение к `clickhouse-02`.
С нуля подъём занимает около трёх минут, на живом стенде — около минуты.
`make smoke` за секунды спрашивает, собран ли стенд: зависимости машины,
здоровье контейнеров, устройство keeper, ответ Kafka с машины через отображённый
@@ -105,7 +110,10 @@ Kafka по той же причине спрашивают снаружи. Её
`make check-clickhouse` запускает отдельную глубокую проверку ClickHouse:
описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`,
`Distributed`, очередь распределённых DDL и очистку временных таблиц.
`Distributed`, очередь распределённых DDL и очистку временных таблиц. Последняя
из девяти проверок — единственная на настоящих данных: она подневно сверяет
события стартового мира с описью и при расхождении говорит, где искать —
в событиях или в браке.
`make config-test` проверяет Compose, синтаксис файлов DAG и пробельные ошибки
в diff без запуска стенда.
@@ -119,6 +127,38 @@ Kafka по той же причине спрашивают снаружи. Её
полного сброса с удалением всех именованных томов используйте `make clean`.
Повторный `make up` безопасен: одноразовая подготовка приложений идемпотентна.
## Стартовый мир
Стенд поднимается не пустым: разовая служба `world-init` играет в топик `hits`
первые восемь дней модельного времени — понедельник по понедельник, 401 185
событий. Дальше их обычным путём разбирает хранилище, и к концу `make up` они
лежат в `ods.event`. Так у всякой лабы есть данные, и всегда одни и те же.
Ждать приходится дольше, чем работает заливка: приём асинхронный, поэтому
вторым шагом `make up` зовёт `scripts/wait-for-world.sh` — тот опрашивает
ClickHouse, пока мир не доедет. Повторный `make up` заливает мир заново; это
не ошибка, а свойство: номера событий те же, и повтор схлопнет
`ReplacingMergeTree`.
Сам мир в git не хранится — он чистая функция зерна, и держать его в
репозитории значило бы держать там кэш. Вместо него лежит **опись мира**,
[`data/world-inventory.json`](data/world-inventory.json): зерно, версия
генератора, хеш каталога товаров и по строке на каждый день — дата, число
событий и хеш его байтов. Опись отвечает на единственный вопрос: тот ли это
мир, что был вчера.
Спрашивают её двое. `make test` сверяет опись с тем, что собирается из кода
сегодня: правка генератора меняет мир, и опись надо пересобрать —
`make inventory`. `make check-clickhouse` сверяет с описью то, что доехало до
`ods.event`, подневно. Хеш дня можно пересчитать и руками — это обычный
`sha256sum` файла, который пишет файловый приёмник:
```bash
uv run --project generator python -m clickstream_generator batch \
--day 0 --file tmp/day0.jsonl
sha256sum tmp/day0.jsonl
```
## Как позвать генератор
События производит генератор из [`generator/`](generator/). Модельный день —
@@ -160,6 +200,10 @@ make generate-live GENERATOR_DAY=3 GENERATOR_SPEED=1000
docker compose --profile generator build generator
```
Тот же образ несёт заливка стартового мира, поэтому свежим его держит и
обычный `make up`: он собирает образы всего стенда, и генератор теперь среди
них.
Разведено это нарочно: собрать образ и запустить контейнер — разные действия, и
цель запуска, молча пересобирающая образ, стирает между ними границу. Что образ
устарел, видно по собственному прогону — это обратная связь, а не ловушка.