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

255 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Проверки: карта целей
Документ отвечает на два вопроса. Первый: что именно утверждает каждая цель
`make` и сколько стоит её прогон. Второй, ради которого документ и заведён:
куда положить новую проверку — так, чтобы это решалось по карте, а не чтением
скриптов.
Зона ответственности у документа одна — проверки. Что именно они стерегут,
описано в других местах: устройство хранилища — в
[storage.md](storage.md), замысел стенда — в спеке [«Боевой реализм стенда
(v2)»](../specs/2026-07-30-stand-v2-realism.md).
## Ось: кого спрашивают
Цели различаются не ценой, а тем, к кому обращён вопрос. 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` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [опись мира](../../data/world-inventory.json) — свежими | не нужен | 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](../adr/0004-resource-limits.md) разбирает ровно этот случай — оценка
из спеки попала жёстким порогом в `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 остался и по другому основанию —
их на машине не запускает никто.