Files
clickstream-data-platform/docs/architecture/testing.md
T
ddadmin 7c9eeedc40 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
2026-08-07 18:37:01 +03:00

23 KiB
Raw Blame History

Проверки: карта целей

Документ отвечает на два вопроса. Первый: что именно утверждает каждая цель make и сколько стоит её прогон. Второй, ради которого документ и заведён: куда положить новую проверку — так, чтобы это решалось по карте, а не чтением скриптов.

Зона ответственности у документа одна — проверки. Что именно они стерегут, описано в других местах: устройство хранилища — в storage.md, замысел стенда — в спеке «Боевой реализм стенда (v2)».

Ось: кого спрашивают

Цели различаются не ценой, а тем, к кому обращён вопрос. ClickHouse отвечает сам и за миллисекунды. Airflow отвечает через такт планировщика. Superset — через сессию и обход метаданных. Быстрота выходит следствием этого различия, а не критерием деления.

Совпадение цены и назначения здесь побочное, и это видно по дырам, которые каждая цель оставляет соседке. make smoke не заметит перепутанных местами макросов shard: обе ноды здоровы и порты отвечают. make check-clickhouse не заметит потерянного подключения Superset: он про ClickHouse и только.

Отсюда правило для новой проверки: спроси, кого она спрашивает. Счётчики против описи — вопрос к ClickHouse: строки в ods.event считает сам сервер и отвечает сразу, значит дом им в check-clickhouse, даже если по цене они подошли бы смоуку.

Карта целей

Стенд нужен трём целям из семи. Цена — замер 7 августа 2026 года, см. «Что проверено».

Цель Что утверждает Стенд Цена
make lint Код генератора отформатирован и проходит ruff не нужен 0,4 с
make typecheck Типы генератора сходятся (ty) не нужен 0,5 с
make test Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода описание выгрузки и опись мира — свежими не нужен 71 с
make config-test Compose разбирается, файлы DAG синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего не нужен 1 с
make smoke Стенд собран: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук нужен 9 с
make check-clickhouse Всё, что спрашивают у ClickHouse и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, счёт событий стартового мира против описи нужен 8 с
make check-services Службы работают: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу нужен 44 с

Цена самого make up — 2 м 50 с с нуля (make clean перед ним — ещё 23 с) и 1 м 5 с на живом стенде. В цену целей она не входит, но записана здесь по той же причине: с #42 подъём стенда заливает стартовый мир и стал заметно дороже.

Правило: смоук обязан оставаться быстрым

make smoke — быстрая проверка для регулярного прогона: её гоняют не задумываясь, и потому она обязана укладываться в секунды. Тяжёлые проверки в неё включать не следует: тяжёлая — та, из-за которой смоук перестаёт быть быстрым. Поодиночке это единицы и десятки секунд, вместе — минуты. Такие проверки живут в make check-services.

На практике дорого обходится не работа, а ожидание службы: такт планировщика Airflow, сессия Superset. make check-clickhouse создаёт таблицы, вставляет строки и гоняет распределённые DDL — и укладывается в семь секунд, потому что ClickHouse отвечает сразу.

Первый абзац — правило, второй — наблюдение, по которому тяжёлую проверку узнают заранее, не замеряя.

Какую проверку когда запускать

Деление целей ценно ровно до тех пор, пока оно не превратилось в «гонять всегда всё».

Когда Что гонять
Правка в работе make config-test и make smoke
PR или задача плюс цели, которых правка касалась: DAG-и, Superset или Kafka — check-services; DDL, кластер или данные — check-clickhouse
Приёмка этапа make up с нуля и все три цели на стенде

Правки генератора добавляют к этому make lint, make typecheck и make test: стенд им не нужен, а make test из них самая дорогая.

Тринадцать секунд из её семидесяти одной — пересборка восьми модельных дней для сверки с описью мира. Дешевле хеши не сравнить: чтобы узнать, тот ли получается мир, его надо получить. Место выбрано по той же оси «кого спрашивают» — вопрос обращён к коду генератора, стенд ему не нужен, — и краснеет проверка там, где надо: сразу после правки генератора. Не будь её, расхождение всплыло бы получасом позже, на поднятом стенде, где выглядит поломкой хранилища, а не забытой пересборкой.

Порогов по времени здесь нет

Цена в таблице — замеренное число с датой замера, а не назначенный порог. Автоматической проверки времени в репозитории нет и заводить её не следует. Число, вписанное в проверку, становится законом, которого никто не выбирал: ADR 0004 разбирает ровно этот случай — оценка из спеки попала жёстким порогом в make smoke, make smoke стал критерием приёмки каждого этапа, и дальше решения сверялись уже с порогом, а не с исходным доводом. Спека генератора формулирует ту же позицию прямо: «наблюдаемость без порогов».

Смотрит на время человек. make smoke и make check-services печатают его сами — последней строкой ИТОГ: пройдено N, ошибок M, время T с. Замер при приёмке делается руками и называется в теле PR.

Проверка, которая не умеет краснеть, бесполезна

Правило, которое стоит держать в голове, правя любую проверку. Проверка, никогда не видевшая своей поломки, доказывает только то, что она умеет печатать «ЗЕЛЁНО». Убедиться дешевле всего руками: сломайте то, что она стережёт — остановите prometheus, удалите служебную таблицу пробника на второй ноде, — и посмотрите, покраснеет ли прогон и назовёт ли виновника. Не покраснел — проверка не работает, и чинить надо её, а не стенд.

Интеграционная проверка постоянной целью не становится

Проверка, которая не спрашивает стенд, а кормит его — шлёт сообщения, вставляет строки, портит данные нарочно, — постоянной целью make не живёт. Такую проверку гоняют один раз при исполнении, убеждаются, что механизм работает, и оставляют след записью в теле PR. Регулярная жизнь стенда — это жизнь с менти, а интеграционный прогон в неё не входит; самое большее, он становится темой урока.

Довод не про цену прогона, а про данные. Вложенное остаётся: у сырья срок жизни трое суток, а ODS хранит всё, и события, которых мир не рождал, лягут в лабы менти неотличимо от настоящих — считая конверсию, он молча посчитает и их. Хуже явного мусора: явный видно.

Отсюда правило для того, что всё-таки вкладывает: прибираться за собой. Дешевле всего это выходит партицией — опыт играет модельный день за границей оси мира, его строки ложатся в собственную партицию EventDate, и она сносится по окончании. Заодно менти видит партицию как единицу работы — тот же механизм, на котором стоит переобработка дня X.

Что тогда стережёт цепочку постоянно — проверки на настоящих данных, тех, что стенд произвёл сам: подневный счёт событий против описи мира ловит и сломанный разбор (события уехали в брак — счёт разошёлся), и потерю по дороге, ничего при этом не вкладывая. Это единственный постоянный сторож цепочки Kafka → STG → ODS, и живёт он в make check-clickhouse.

Правило, которое такая проверка обязана выдержать и которое стоит держать в голове для следующей: на растущих данных утверждать можно только про зафиксированный кусок. Мир растёт — менти переиграет день, этап 5 добавит следующий, — и всякое утверждение про таблицу целиком однажды покраснеет законно, то есть впустую. Как это сделано здесь, написано в самом скрипте.

Корректность процессов живёт в дагах DQ, а не в целях make

Цели make отвечают на вопрос «стенд собран и работает». Вопрос «данные в нём правильные» — работа отдельных дагов качества данных: они приходят вместе со следующими уровнями хранилища и заберут многое из того, что просится проверкой сейчас. Поэтому находка «это стоило бы проверять регулярно» сначала проверяется вопросом, не туда ли ей: если она про свойства данных — про полноту дня, про свежесть слоя, про сходимость витрины с источником, — её дом даг, а не check-clickhouse.

Разница видна и менти, и она содержательная. Цель make он зовёт руками, когда чинит стенд; даг DQ идёт по расписанию рядом с остальным конвейером, пишет историю проверок и разбирается как обычная задача Airflow — то есть учит тому, как качество данных устроено в бою.

Устройство скриптов

Цель Скрипт
make config-test scripts/config-test.sh
make smoke scripts/stand-smoke.sh
make check-services scripts/stand-services.sh
make check-clickhouse scripts/check-clickhouse.sh

Особняком — scripts/wait-for-world.sh: он не проверка, а вторая половина make up. docker compose up --wait дожидается служб, в том числе успешно отработавшей заливки, но «заливка кончилась» значит «события в топике», а не «события в хранилище»: приём асинхронный. Скрипт ждёт доезда ограниченным циклом опроса. Без него make check-clickhouse следом краснел бы по устройству, а не по поломке.

Общее у смоука и check-services — счёт проверок, обращение к Compose и две проверки — вынесено в scripts/stand-common.sh; сам он не запускается. Оттуда же приходят два решения, которые видно по счёту прогонов:

  • check_containers_survived стоит в конце обеих целей. Убитый за память контейнер Docker поднимает сам, и проверка здоровья об этом промолчит (ADR 0004). Стенд нагружает check-services, а увидеть последствия нужно и тому, кто гонял один смоук. Это единственная проверка, которая считается дважды: 20 у смоука плюс 7 у check-services — это 26 разных проверок.
  • Зависимости машины считает только смоук. «На машине есть Docker, curl и jq» — вопрос к машине, а не к службам, и на оси он стоит рядом с «стенд собран». Для check-services это условие запуска: без них он не начнёт работу и громко скажет об этом, но ЗЕЛЁНО за это не печатает.

Вход в Airflow check-services выполняет заново — общих переменных у двух скриптов нет. Отдельной проверкой этот вход тоже не считается: то же самое утверждает смоук.

Что проверено

Перезамер 7 августа 2026 года при исполнении #42. Подъём стенда с нуля (make clean && make up) — 2 м 50 с, из них 22,5 с занимает сама заливка: 18,7 с генерация восьми дней и 3,8 с доставка 401 185 сообщений в Kafka. Повторный make up на живом стенде — 1 м 5 с: мир заливается заново, и это не оплошность — WatchID те же, повтор схлопнет ReplacingMergeTree. Смоук — 9 с, check-clickhouse — 8 с с новой девятой проверкой. make test вырос до 71 с: 58 с прежних тестов плюс 13 с на пересборку восьми дней для сверки с описью. Прежние 40 с в таблице устарели ещё до #42 — тесты добавляли #41 и #43.

Что новая проверка умеет краснеть, снято двумя поломками того же дня, и проверялись обе ветви её диагноза. Снесли партицию 2026-06-03 в ods.event_rep — проверка покраснела, показала недостающий день и назвала адрес: «события не доехали до ODS, начните с чтеца топика». Затем положили в сырьё заведомо негодную строку — брак появился, и проверка сменила диагноз на «сломан разбор, начните с ods.event_errors_dist». Строки опыта убраны, день переигран make generate-batch GENERATOR_DAY=2; счёт вернулся к 401 185, и это заодно показало дедупликацию: повтор дня не удвоил счёт под FINAL.

Замеры 6 августа 2026 года, стенд поднят заранее. Время взято по time и совпадает с тем, что цель печатает сама. Оно плавает от прогона к прогону: смоук дал 8 секунд дважды (без проверки Kafka, до её появления, — 5 и 6), check-services — 43 и 44. В таблице стоит большее из замеренных.

До деления scripts/stand-smoke.sh шёл 48 секунд на 25 проверок, из них 42 секунды съедали шесть: Kafka с машины, два запуска пробников Airflow и три проверки Superset. После деления те же 25 проверок разошлись по двум целям: 19 в смоуке и 6 в check-services. Содержание ни одной из них не менялось.

Двадцатая проверка смоука — единственная новая: Kafka спрашивают с машины через отображённый порт. Деление оставило дыру, которой раньше не было. Про Airflow, Superset, Prometheus и Grafana смоук стучится с машины в отображённый порт, а про Kafka после переезда знал только «контейнер здоров» — а это вердикт проверки состояния из compose.yaml, и та спрашивает брокер изнутри по внутреннему слушателю. Внешняя дверь оставалась непроверенной до check-services с его 44 секундами, хотя генератор пишет в Kafka именно с машины. Стоит проверка 2,6 секунды, и почти всё это — старт JVM в разовом контейнере; ожидание ответа ограничено пятнадцатью секундами, впятеро больше замеренного.

Что обе разделённые цели умеют краснеть, проверено руками в тот же день. Со снятым prometheus смоук дал три ошибки и ненулевой код возврата. С подменённым ожидаемым UUID подключения Superset так же покраснел check-services. Новую проверку Kafka проверили её собственной поломкой: брокеру объявили адрес kafka-nowhere:29092, оставив внутренний слушатель целым, — проверка состояния контейнера осталась зелёной, а смоук покраснел именно на этой строке. Краснеют они и когда на машине не хватает команды из списка зависимостей.

Семантика счётчиков Docker OOMKilled и RestartCount, на которой держится check_containers_survived, снята отдельными контейнерами и записана в ADR 0004, раздел «Что проверено».

Разбор Bash из make config-test срезан 6 августа 2026 года, и вместе с ним — двенадцать строк обхода репозитория со списком файлов. Проверка была сломана с рождения: bash -n со списком файлов разбирает только первый, остальные уходят ему в аргументы, — то есть из пяти скриптов проверялся один, и за всё время цели этого никто не заметил. Заводить её заново незачем: скрипты стенда запускаются с той же машины, и синтаксическая ошибка вылезает при первом же запуске с номером строки. Разбор файлов DAG остался и по другому основанию — их на машине не запускает никто.