- Зачем:
- default должен остаться только учёткой локальных служебных вызовов.
- Что:
- пароль default подставлен из окружения на обеих нодах.
- healthcheck и служебные скрипты передают его внутри контейнера.
- Проверка:
- make config-test, make up, make smoke, make check-clickhouse, make check-services.
- Зачем:
- DDL, Airflow и Superset не должны работать с правами default.
- Что:
- DDL и Airflow переведены на etl, включая явный доступ remote().
- Superset переведён на bi с паролем из окружения.
- etl получил право KAFKA, которое ClickHouse 26.3 требует для движка.
- Проверка:
- make config-test, make lint, make up, make smoke, make check-services.
- Зачем:
- менти должен увидеть разделение доступа без состояния в томах.
- Что:
- пользователи и роли объявлены файлом с паролями из окружения.
- межшардовые запросы передают пользователя через общий секрет.
- четыре допущения реализации подтверждены в ADR живыми замерами.
- Проверка:
- make config-test, make smoke, make check-clickhouse, make check-services.
- Зачем:
- устранены дублирование значений и тихая подстановка неполной настройки.
- Что:
- версии образов и внутренняя топология закреплены рядом с местом использования.
- имя экземпляра, внешние порты, учётные данные и ключи сделаны обязательными настройками .env.
- быстрый старт, Dockerfile и статическая проверка приведены к новой границе.
- Проверка:
- make config-test.
- docker build для образов Airflow и Superset без аргументов.
- make up; make smoke; make check-clickhouse; make check-services.
Зачем
Холодное ревью нашло три места, где написанное сильнее сделанного.
Что
- Непустая таблица брака больше не выдаётся за доказательство сломанного
разбора. Модельного дня у брака нет, обрамить его нечем, и строки прежних
уроков лежат в нём месяц: после первого же урока с мусором проверка
давала бы неверный диагноз навсегда. Теперь она даёт признак, по которому
причину отличают, — сошлась недостача с числом брака или нет.
- Комментарий у world-init обещал, что расхождение числа дней с
STARTING_DAYS поймают счётчики. Это неправда в одну сторону: лишний день
ложится за рамкой дат описи. Обещание убрано, дыра названа.
- Довод «даг next_day этапа 5 продолжит ось» опирался на несуществующий
этап; заменён настоящей причиной — заливка замыкает цепь разовых служб.
- Потолок ожидания 300 с получил обоснование замером с кратностью, а сам
скрипт — честную оговорку: его обещание работает на пустом стенде, на
живом ждать нечего.
- Третья, пропущенная ссылка на снятый порог скорости дня убрана из спеки.
- Даты замеров в карте целей разведены: #42 менял три цели, а не шесть.
Проверка
Обе ветви диагноза сняты заново на живом стенде: без брака — «не доехали»,
с браком — признак различения. Стенд восстановлен, все 9 проверок зелёные,
брака 0. make lint, typecheck, config-test, test (407 тестов) зелёные.
Ссылка: #42
Зачем
Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с
ручной заливки данных. Теперь `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
- Зачем:
- до сих пор генератор умел собирать день, но не умел его отдать: топик
hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит
события до стенда и закрывает форму на проводе, на которую обопрётся
типизированный ODS (#43).
- сериализатор один по решению спеки: второе место, печатающее событие в
JSON, разошлось бы с первым молча.
- Что:
- serialize.py — канонический сериализатор на orjson: единственное место,
где событие целиком становится JSON; 47 ключей всегда, «пусто» это
пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок
ecommerce в commerce.py вторым сериализатором не считается — правило
про событие, а не про блок внутри него.
- sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно
событие — одно сообщение). Ключа у сообщения нет: WatchID уникален,
ключом он был бы ключом лишь на вид.
- player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и
live (темп ×60), несколько дней одним запуском, ограниченная пачка,
раздельные тайминги генерации и доставки, лаг в логе.
- день на оси и имя топика умолчаний не имеют: параметр, описывающий
среду или позицию, приходит от зовущего, иначе отказ до генерации.
Умолчания зерна, числа дней и темпа остаются — они описывают мир.
- generator/Dockerfile — свой образ: зависимости из uv.lock, база
закреплена до патча, раскладка репозитория сохранена ради каталога
товаров. Образ Airflow не тронут.
- разовая служба compose под профилем, цели generate-batch и
generate-live, .dockerignore, tmp/ в .gitignore.
- решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README.
- Проверка:
- make test 406 passed, make lint, make typecheck, make config-test.
- побайтовый детерминизм: два прогона дня в независимых процессах дают
один sha256; день в контейнере совпадает с днём на машине.
- на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по
Distributed сошёлся — отправлено 50626, в таблице 50626.
- топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368),
clickhouse-02 раздел 1 (24258).
- живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й —
темп ×60, лаг печатается.
- форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит
строкой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: стенду нужен воспроизводимый холодный старт, при котором схема
хранилища и топик появляются сами, а сырьё из Kafka доезжает в STG обеими
нодами кластера — без ручных шагов между `make clean` и рабочим приёмом.
Что:
- `sql/ddl/` — три файла, применяются по порядку имён: базы `stg` и `ods`,
Kafka-чтец `hits_raw_kafka` формата RawBLOB, реплицируемая `hits_raw_rep`
с окном TTL в трое суток, распределённая `hits_raw_dist` и матвью
`hits_raw_mv`, переносящая сырьё вместе с метаданными доставки.
- `compose.yaml` — службы `kafka-init` (топик `hits` на две партиции, с
ремонтом уже созданного однопартиционного) и `clickhouse-init` (применяет
`/ddl/*.sql`); `hostname:` у обеих нод, чтобы `hostName()` отдавал имя узла,
а не идентификатор контейнера; `airflow-init` зависит от `clickhouse-init` —
без зависимого успешный одноразовый сервис считается упавшим для `--wait`.
- Доки: конвенции и раздел «Что проверено» в справочнике хранилища, указатели
и границы обещаний в ADR 0005, снятые пункты в разделе 11 спеки.
Проверка: `make lint`, `make typecheck`, `make config-test`, `make smoke`
(25 проверок), `make smoke-guards` — зелёные. Приёмочный прогон с чистого
тома подтвердил все пять критериев #37: холодный старт и идемпотентный
повтор, две партиции у `hits`, метаданные доставки у доехавшего сообщения,
обе партиции на обеих потребляющих нодах в одном прогоне, некорректный JSON
лежит сырым и приём не встаёт.
Известная граница: RawBLOB молча теряет запись с пустым значением и
запись-надгробие; принято как свойство, замер и довод — в справочнике
хранилища.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем
Комментарии к правке были размером с объяснение, хотя объяснение уже лежит
в ADR 0004. В compose.yaml четыре строки на одну настройку; в stand-smoke.sh
одиннадцать новых строк там, где на весь файл до этого было две — шебанг и
одна строка про разбор подстановок. Заодно в комментариях остались метафоры
(«бронь», «предохранители»), вычищенные из ADR прошлым коммитом.
Что
- compose.yaml: одна строка вместо четырёх — почему не гигабайт и куда идти
за подробностями.
- stand-smoke.sh: две строки вместо шести — зачем проверка вообще нужна.
Комментарий про разбор `--format` убран целиком: он оправдывался перед
читателем, а не помогал ему.
- Комментарий про OOMKilled оставлен, но в одну строку: без него сообщение
«убило процесс, а не контейнер» выглядит опиской.
Проверка
make config-test — зелено.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем
Стенд упирался в память ноды ClickHouse: пробник валился на CREATE TABLE
ON CLUSTER, вместе с ним краснели make smoke и make smoke-guards. Причина не
та, что предполагал #21: дело не в заводских кэшах, а в коробке на гигабайт.
Около 550 МиБ RSS праздной ноды — страницы её собственного бинарника, и на
работу оставалось около 350 МиБ, которые пробник добирал за сессию.
Заодно выяснилось, откуда взялся предел 3,4 ГБ. Это была оценка расхода из
спеки, посчитанная по стенду-предшественнику до первой сборки v2 и превращённая
в жёсткий порог проверки. Порог стал критерием приёмки каждого этапа и дальше
блокировал бы любой рост стенда на этапах 2-9.
Что
- ADR 0004: бюджета памяти у стенда нет, есть требование к машине — около 8 ГБ,
доступных Docker. Ресурсный довод ADR 0001 отозван, сами решения в силе.
- Нодам ClickHouse 4 ГиБ вместо гигабайта. Остальные лимиты не тронуты: ни один
из них ни разу не сработал, а снять их скопом — то же изменение без
свидетельств, каким они были выставлены.
- Из make smoke убрана проверка суммарного потребления. Она мерила docker stats
вместе со страничным кэшем, то есть отвечала на вопрос «сколько файлов стенд
потрогал», и с появлением настоящих данных краснела бы на здоровом стенде.
Вместе с ней убрана привязанная к её сообщению проверка docs-guards.
- Взамен smoke спрашивает у Docker, не убивало ли ядро долгоживущий контейнер
за память и не включалась ли политика перезапуска. Порога у проверки нет:
убитый контейнер Docker поднимает сам, и без этого вопроса стенд отрапортует
«всё хорошо» о ноде, которая умирала.
- README и раздел «Ресурсный бюджет» спеки переписаны с предела на требование
к машине; README объясняет менти, что такое «память, доступная Docker».
Проверка
make config-test; make up; make smoke — 25 из 25; make smoke-cluster — 8 из 8;
make smoke-guards — 3 из 3, включая шаг «после восстановления стенд проходит
make smoke», который падал 31 июля.
На живом стенде с новой коробкой: max_server_memory_usage = 3,60 ГиБ, в журнале
ноды «Lowered mark cache size to 2.00 GiB because the system has limited RAM».
Семантика счётчиков Docker снята отдельными контейнерами: ручной restart
оставляет RestartCount = 0, убийство за память даёт OOMKilled = true и растущий
счётчик, убийство не за память OOMKilled не поднимает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем.
Демонстрационный DAG example_clickstream_hello ничего не проверял: он не
обращался ни к ClickHouse, ни к Kafka, поэтому его зелёный результат ничего
не говорил о стенде. Пробники проверяют связи по-настоящему — и тем же
клиентом, каким будут ходить рабочие DAG.
Что.
- test_clickhouse: пишет строку в ReplicatedMergeTree на ноде 1 и читает её
с ноды 2 через Distributed. Данные проходят путь «нода 2 → все шарды →
шард ноды 1», то есть проверяется межшардовое чтение, а не одна нода.
- test_kafka: пишет в постоянный топик сообщение с меткой прогона и
вычитывает его обратно.
- infra/airflow/Dockerfile: clickhouse-connect 1.6.0 и confluent-kafka
2.15.0 вшиты в образ, импорт проверяется на сборке — при запуске
контейнера пакеты не доустанавливаются.
- Проверки: scripts/stand-smoke.sh гоняет оба пробника через API Airflow,
scripts/config-test.sh разбирает DAG без стенда,
tests/stand-smoke-guards.sh проверяет красный путь,
tests/dag-probes-unit.py — модульные проверки разбора.
- README и ADR 0001 обновлены тем же изменением.
- Удалён dags/example_clickstream_hello.py.
Проверка.
make config-test — пройдено 3, 3 и 6, ошибок 0.
make clean; cp .env.example .env; make up — 116 с на чистых томах.
make smoke — пройдено 25, ошибок 0; стенд занимает 2244,0 MiB.
make smoke-cluster — все 8 проверок кластера.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Зачем: рядом с кластером ClickHouse не хватало остальной платформы, а
поднимать её по кускам — значит каждый раз вспоминать порядок. Теперь
`make up` даёт стенд целиком, а `make smoke` честно отвечает, работает он
или нет.
Что:
- Kafka в режиме KRaft (без ZooKeeper), Postgres под метаданные, Airflow
3.3 четырьмя сервисами и Superset 6.1 с драйверами ClickHouse;
- мониторинг: Prometheus снимает метрики с обеих нод ClickHouse и keeper,
Grafana получает подготовленный источник данных;
- подключение Airflow ведёт на ноду 1, подключение Superset — на ноду 2:
ловушка правильных ошибок, забытый `ON CLUSTER` виден в дашборде сам;
- `scripts/stand-smoke.sh` — сквозная проверка из 24 пунктов: топик в
Kafka, цели Prometheus, запуск примера DAG через API Airflow, проверка
подключения Superset и расход памяти против порога 3,4 ГБ;
- `make config-test` — статические ворота: Compose, синтаксис Bash и
Python, стражи README; стражи smoke проверяют, что отчёт краснеет на
сломанном стенде и зеленеет после восстановления;
- решения записаны в `docs/adr/0001-stand-services.md`, состав стенда и
порядок работы — в README.
Проверка: на чистых томах `make clean` → `cp .env.example .env` →
`make up` (1 мин 51 с) → `make smoke` — 24 пройдено, 0 ошибок, 2171 MiB.
Перезапуск `make down` → `make up` → `make smoke` — 24/0. Также зелены
`make config-test` (3/0 и 5/0), `make smoke-cluster` (8/8) и
`make smoke-guards` (3/0 и 3/0).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- этап 1 спеки требует стенд, поднимаемый одной командой; кластер —
единственный режим, выключателя «без кластера» нет (issue #12).
- Что:
- compose.yaml: две ноды ClickHouse и отдельный clickhouse-keeper на
зафиксированном LTS-образе 26.3.17.56, порты только на 127.0.0.1.
- infra/clickhouse: общее описание кластера, подключение к keeper и
отдельные макросы shard и replica для каждой ноды.
- scripts/clickhouse-smoke.sh: восемь проверок ON CLUSTER от описания
кластера до удаления временных таблиц, вывод по-русски.
- tests/smoke-guards.sh: три проверки самой smoke-команды —
ограниченная аварийная очистка, обработка прерывания, окружение keeper.
- README.md: быстрый старт, роли нод, обоснование выбора версии.
- Проверка:
- make up && make smoke && make smoke-guards && make clean