Files
clickstream-data-platform/docs/architecture/testing.md
T
ddadminandClaude Fable 5.1 59d8c83b17 docs(glossary): четыре термина переименованы, world.md и stand.md вычитаны
- Зачем:
  - справочники читает человек со стороны, а «пульт мира», «опись мира»,
    «работник» и «сырьё» звучали шуткой или калькой времён разработки
    (тикет #214, выбор владельца).
- Что:
  - CONTEXT.md: статьи «Паспорт мира», «Даги модельного времени»,
    «Шаговый даг», «Сырые данные», старые слова в строках «Избегать»;
    пометки в ADR 0005, 0008, 0009 без переписывания решений.
  - замены по README, справочникам, путеводителю, лабам, спекам,
    комментариям дагов и SQL, тегу дага и панели Grafana; имена в коде
    не тронуты.
  - world.md и stand.md: 35 находок вычитки внесены, регистр выровнен.
- Проверка:
  - make config-test; make lint в корне и в generator/; make test в
    generator/ (437 passed); скрипт ссылок из #212 пуст; grep старых
    слов вне ADR, research и CONTEXT.md пуст.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QBCtU38qSHg5DvgtnKXcW
2026-09-02 16:46:50 +03:00

557 lines
49 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`, даже если по цене они
подошли бы смоуку.
## Карта целей
Цели живут за двумя дверями. В корне — цели стенда; в `generator/` — цели
генератора: он отдельный пакет со своим `pyproject.toml`, локом и образом, и
спрашивают его отдельно.
Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено».
`make smoke` и `make check-services` перемерены 28 августа 2026 года,
`make config-test` — 21 августа, `make check-clickhouse` — 19 августа,
корневой `make lint` — 9 августа, `make test` — 30 августа; остальные стоят с
замера 6 августа.
| Цель | Откуда | Что утверждает | Стенд | Цена |
|---|---|---|---|---|
| `make config-test` | корень | Compose разбирается, файлы DAG и дашборды Grafana синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с |
| `make lint` | корень | Код стенда — `dags/` и `infra/superset/` — отформатирован и проходит ruff | не нужен | 0,4 с |
| `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 25 с |
| `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, ответ словаря товаров, счёт событий стартового мира против паспорта | нужен | 24 с |
| `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 71 с |
| `make lint` | `generator/` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с |
| `make typecheck` | `generator/` | Типы генератора сходятся (ty) | не нужен | 0,5 с |
| `make test` | `generator/` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [паспорт мира](../../data/world-inventory.json) — свежими | не нужен | 176,5 с |
**Строк с именем `make lint` две, и это не опечатка.** Одноимённые цели за
разными дверями охватывают разное: корневая не видит ни одного файла
генератора, генераторная — ни одного файла стенда. Человек в корне видит
зелёное и может решить, что зелёный весь репозиторий.
Две тонкости, которых не видно из таблицы:
- **Версия ruff у корневой цели закреплена в самом вызове** (`uvx ruff@0.16.1`)
и равна той, что в `generator/uv.lock`. Равенство держится руками: обновили
лок — поправьте и вызов, сверять их некому.
- **Охват держат аргументы вызова, а не устройство дерева.** Python,
положенный вне двух названных в таблице путей, не проверит ни одна из дверей.
**Корневого `typecheck` нет, и это решение, а не пропуск.** Проверке типов мало
прочитать файл: чтобы понять `from airflow.sdk import dag`, ей нужен
установленный Airflow, а он живёт в образе, не на машине — 132 пакета ради двух
файлов пробников (замер 9 августа 2026 года, `uv pip compile`). Цена решения
называется честно: две настоящие находки `ty` в `dags/test_kafka.py` постоянного
сторожа не получают. Вопрос вернётся на этапе 5, когда придут настоящие даги:
тогда окружение окупится, и условие возврата — именно это, а не «стало
неудобно».
Цена самого `make up` с пустых томов — 8 м 18 с до переезда жизненного цикла в
Airflow. В цену целей она не входит. На готовом мире подъём больше не
пересчитывает данные: `world_initialize` зеленеет без изменений.
`make up` собирает локальные образы Airflow, Superset и Grafana, поднимает весь
стенд и ждёт здорового состояния долгоживущих контейнеров. В образ Airflow
добавлены закреплённые клиенты ClickHouse и Kafka. Одноразовые `airflow-init` и
`superset-init` завершаются с кодом 0. Первый обновляет схему Airflow,
подготавливает администратора и два подключения к `clickhouse-01`. Второй
обновляет Superset, создаёт администратора и импортирует подключение к
`clickhouse-02`. На ноутбуке в режиме энергосбережения приёмочный подъём с
пустых томов занял около десяти минут, повторный на живом стенде — около трёх.
`make rebuild-storage` тоже не проверка. Команда через Airflow удаляет
прикладные базы ClickHouse, топики и позиции групп Kafka, заново применяет DDL,
заливает стартовый мир и возвращает его позицию к концу стартовых дней. Службы и
их тома она сохраняет, поэтому возврат к каноническим данным не требует
холодного запуска всего стенда.
## Правило: смоук обязан оставаться быстрым
`make smoke` — быстрая проверка для регулярного прогона: её гоняют не
задумываясь, и потому она обязана укладываться в секунды. Тяжёлые проверки в
неё включать не следует: тяжёлая — та, из-за которой смоук перестаёт быть
быстрым. Поодиночке это единицы и десятки секунд, вместе — минуты. Такие
проверки живут в `make check-services`.
На практике дорого обходится не работа, а ожидание службы: такт планировщика
Airflow, сессия Superset. `make check-clickhouse` создаёт таблицы, вставляет
строки и гоняет распределённые DDL — и укладывается в семь секунд, потому что
ClickHouse отвечает сразу.
Первый абзац — правило, второй — наблюдение, по которому тяжёлую проверку
узнают заранее, не замеряя.
## Какую проверку когда запускать
Деление целей ценно ровно до тех пор, пока оно не превратилось в «гонять
всегда всё».
Состояние перед проверкой восстанавливается в самой узкой достаточной области:
сначала объект или партиция, затем база слоя. Если нужен целиком чистый
прикладной мир, `make rebuild-storage` пересобирает его базы, топики, позиции
потребителей и данные. `make clean` нужен, только когда меняется или сломано
постоянное состояние вне этих баз либо проверяется сам запуск с пустых томов.
Требование к обоснованию полного сброса задаёт `AGENTS.md`. Неизвестную поломку
сначала исследуют: полный сброс уничтожает её следы.
| Когда | Что гонять |
|---|---|
| Правка в работе | `make config-test` и `make smoke` |
| PR или задача | плюс цели, которых правка касалась: DAG-и, Superset или Kafka — `check-services`; DDL, кластер или данные — `check-clickhouse` |
| Приёмка этапа | все три цели на стенде; запуск с пустых томов — только для изменений начальной сборки или постоянного состояния служб |
Правки генератора добавляют к этому `make lint`, `make typecheck` и `make test`
из каталога `generator/`: стенд им не нужен, а `make test` из них самая
дорогая. Правки дагов и Superset — корневой `make lint`.
Двадцать секунд из ее ста семидесяти шести — пересборка восьми модельных дней
для сверки с паспортом мира (20,5 с из 176,5). Дешевле хеши не сравнить: чтобы
узнать, тот ли получается мир, его надо получить. Место выбрано по той же оси
«кого спрашивают» — вопрос обращён к коду генератора, стенд ему не нужен, — и
краснеет проверка там, где надо: сразу после правки генератора. Не будь её,
расхождение всплыло бы получасом позже, на поднятом стенде, где выглядит
поломкой хранилища, а не забытой пересборкой.
## Порогов по времени здесь нет
Цена в таблице — замеренное число с датой замера, а не назначенный порог.
Автоматической проверки времени в репозитории нет и заводить её не следует.
Число, вписанное в проверку, становится законом, которого никто не выбирал:
[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/run-dag.sh`: это не проверка, а общий REST-переход от
`make` к Airflow. `make up` запускает через него `world_initialize`, а
`make rebuild-storage` — `world_recreate`; сценарий ждёт появления дага в API,
а затем до минуты повторяет только стартовый ответ `Dag with dag_id not found`.
Прочие ответы и сетевые ошибки он не повторяет: при потерянном ответе запуск мог
уже создаться. Сценарий печатает идентификаторы дага и прогона и возвращает
вызывающему его итог. Ожидание доставки событий и запуск `orders_ingest` теперь
видны задачами `world_initialize`, а не спрятаны в shell.
Общее у смоука и `check-services` — счёт проверок, обращение к Compose и две
проверки — вынесено в `scripts/stand-common.sh`; сам он не запускается.
Оттуда же приходят два решения, которые видно по счёту прогонов:
- **`check_containers_survived` стоит в конце обеих целей.** Убитый за память
контейнер Docker поднимает сам, и проверка здоровья об этом промолчит
(ADR 0004). Стенд нагружает `check-services`, а увидеть последствия нужно и
тому, кто гонял один смоук. Это единственная проверка, которая считается
дважды: 21 у смоука плюс 7 у `check-services` — это 27 разных проверок.
- **Зависимости машины считает только смоук.** «На машине есть Docker, curl и
jq» — вопрос к машине, а не к службам, и на оси он стоит рядом с «стенд
собран». Для `check-services` это условие запуска: без них он не начнёт
работу и громко скажет об этом, но ЗЕЛЁНО за это не печатает.
Вход в Airflow `check-services` выполняет заново — общих переменных у двух
скриптов нет. Отдельной проверкой этот вход тоже не считается: то же самое
утверждает смоук.
### Что спрашивает смоук
`make smoke` за секунды спрашивает, собран ли стенд: зависимости машины,
здоровье контейнеров, устройство keeper, ответ Kafka с машины через отображённый
порт, четыре цели Prometheus, два источника Grafana, компоненты Airflow и
подготовленное подключение к `clickhouse-01`.
Двенадцать проверок здоровья сразу после `make up --wait` повторяют то, чего
Compose уже дождался: у каждой долгоживущей службы есть своя `healthcheck`.
Оставлены они потому, что первый вопрос к стенду всё равно «всё ли живо», а
ответ на него стоит меньше секунды. Устройство keeper — другое дело: он должен
работать от пользователя `clickhouse`, с пределом в 262144 открытых файла и со
своим томом под данные. Всё это объявлено в `compose.yaml`, но здоровым keeper
выглядит и без этого, поэтому смоук спрашивает у живого контейнера, дошли ли
объявленные настройки до процесса.
Kafka по той же причине спрашивают снаружи. Её `healthcheck` обращается к
брокеру изнутри контейнера и по внутреннему слушателю, поэтому здоровой Kafka
остаётся и тогда, когда объявленный наружу адрес ведёт не туда. Клиент с машины
в этом случае подключается, получает метаданные и молча виснет на адресе,
которого с его стороны не существует. Смоук идёт тем же путём, что и будущий
генератор: стучится в отображённый порт и просит список топиков.
### Что спрашивают глубокие проверки
`make check-services` проверяет то, ради чего приходится ждать службу: работу с
топиком Kafka с машины — создан, найден, удалён, — ручной запуск пробников
`test_clickhouse` и `test_kafka`, вход в Superset, его метаданные и подключение
к `clickhouse-02`. Первый пробник создаёт таблицы на обеих нодах и читает через
`Distributed` на ноде 2 строку из локальной таблицы ноды 1. Второй пишет в Kafka
и читает свой маркер. Временный топик проверки с машины и запуски DAG удаляются;
постоянный топик пробника сохраняется, а старые записи чистит Kafka.
`make check-clickhouse` запускает отдельную глубокую проверку ClickHouse:
описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`,
`Distributed`, очередь распределённых DDL и очистку временных таблиц. Девятая из
десяти проверок спрашивает словарь товаров на обеих нодах. Последняя — подневный
счёт событий против паспорта, о нем сказано выше.
## Что проверено
**Калибровка заказной стороны 30 августа 2026 года при исполнении #194.**
Пересборки эталонного мира не будет (#192), поэтому доли заказной стороны
сверены замером внутренностей генератора — окно 28 модельных дней,
канонический seed, без транспорта, — тем же приемом, каким `test_plan.py` и
`test_commerce.py` меряют мир. Команда (из `generator/`):
```bash
uv run python - <<'PY'
from collections import Counter
from clickstream_generator import day, inventory, orders, world
from clickstream_generator.seeds import CANONICAL_SEED
WINDOW = 28
class_totals: Counter[str] = Counter()
outcome_totals: Counter[int] = Counter()
delivery_totals: Counter[int] = Counter()
delay_totals: Counter[int] = Counter()
total_orders = 0
for d in range(WINDOW):
today = day.stream(CANONICAL_SEED, d)
for key, value in inventory._order_class_counts(today).items():
class_totals[key] += value
money = today.orders
total_orders += len(money)
for value in money.outcome.tolist():
outcome_totals[value] += 1
for value in money.delivery.tolist():
delivery_totals[value] += 1
for value in money.snapshot_delay.tolist():
delay_totals[value] += 1
print(total_orders, dict(class_totals), dict(outcome_totals),
dict(delivery_totals), dict(delay_totals))
PY
```
6973 заказов за 28 дней. Доли из таблицы сверки (после приоритета классов):
отменено 5,15% (ориентир мастер-спеки ~5%, в пределах; из них 3,03%
оплачен-потом-отменен и 2,12% отменен неоплаченным — веса `world.py`
95/3/2); дубль `purchase` 1,89% (ориентир ~2%, в пределах); дельта суммы
1,32% (ориентир 1–2%, в пределах); потеря `purchase` 1,78% при броске 3% —
вычеты штатные: назначенные планом покупки из броска исключены (мост
склейки), часть случаев уходит в отмену приоритетом; наблюдаемая доля
принята решением владельца (#194). Стоимость доставки — 35,5% / 49,2%
/ 15,4%, совпадает с весами `DELIVERY_KOPECKS_WEIGHTS`. Опоздание слепков
(`ORDER_SNAPSHOT_DELAY_WEIGHTS`) — доля заказов с δ>0 составила 8,39%
(ориентир мастер-спеки ~10%, в пределах). Скрипт замера в репозиторий не
входит; вывод — в отчете issue #194.
**Перезамер 30 августа 2026 года при исполнении #192.** Команды запускались
из `generator/`:
```bash
time make inventory
uv run python -m clickstream_generator batch --day 0 --days 8 \
--file /tmp/stage7-events.jsonl
for snapshot_day in 1 2 3 4 5 6 7; do
uv run python -m clickstream_generator snapshot --day "$snapshot_day" \
--file "/tmp/stage7-snapshot-${snapshot_day}.jsonl"
done
wc -c /tmp/stage7-events.jsonl /tmp/stage7-snapshot-{1,2,3,4,5,6,7}.jsonl
time make test
```
Паспорт пересобрался за 20,515 с. Файловый приемник записал 460 731 812 байт:
458 478 771 байт событий и 2 253 041 байт слепков. `make test` прошел 437/0 за
176,476 с. Прежние 71 с (7 августа, #42) выросли по двум причинам: тестов стало
437 против 407, и потяжелел сам мир — та же пересборка восьми дней стоила 13 с,
теперь 20,5. Эта правка добавила один тест — проигрыш дня.
**Приемка #172 28 августа 2026 года.** Холодный `create_app()` занимал
28,3-28,4 секунды, а чтение подключения после запуска - 0,07 секунды. Предел
30 секунд почти целиком уходил на запуск второго приложения. Увеличивать его
не стали: Superset не обещает постоянного времени `create_app()`.
После входа проверка делает три GET-запроса к уже работающему Superset: находит
`ClickHouse`, сверяет UUID и безопасный URI, затем требует схему `dm`. Так без
второго запуска проверяются сохраненное подключение и хранилище пароля. Форму
API сверили через Context7 и с исходным кодом Superset 6.1.0. `superset test-db`
отклонен, потому что тоже запускает приложение; прямой SQL к `dbs` не проверяет
соединение.
Проверка больше не подтверждает, что сам Superset использует Postgres для
метаданных. Короткий запрос к Postgres показывает лишь готовность базы, а REST
API - подключение к ClickHouse. Для `check-services` этого достаточно.
После перезапуска Superset три прогона прошли 7/7 за 71, 67 и 70 секунд.
`make config-test` прошел. `make smoke` дал 21/21 за 25 секунд; его код не
менялся, прежний замер был 17 секунд. Неверный UUID дал 5/1: красной стала
только проверка метаданных, пароль в вывод не попал.
**Приемка #156 27 августа 2026 года.** `make rebuild-storage` занял 7 минут.
На чистом прикладном мире `world_initialize` сам довел позицию до `8`, дождался
`etl_pipeline` и оставил заполненными DDS и обе витрины. После пересборки прошли
`make smoke` 21/21 и `make check-clickhouse` 10/10. Перед стендовыми сценариями
прошли `make config-test` и `make lint`.
Два прогона `world_next_day` не изменили слои; один ручной `etl_pipeline` без
параметров догнал оба дня. Полный прогон `world_live_day` занял 27 м 2 с и
завершился только после успешных DDS и DM.
Временная синтаксическая поломка `sql/dm/daily_traffic_rebuild.sql` дала
красную цепочку: DDS завершился успешно, а `daily_traffic`, DM, `etl_pipeline`
и `world_live_day` покраснели. После возврата исправного SQL ручной
`etl_pipeline` без параметров завершился успешно на позиции `12` и восстановил
слои.
Для проверки пакетного барьера `stg.hits_raw_mv` временно отсоединили командой
`DETACH` на обеих нодах. После `world_next_day` отставание Kafka составило
46 367 (19 128 + 27 239). Во время ожидания матвью вернули командой `ATTACH`.
Журнал готовности записал повторные опросы с отставанием 46 367, затем 27 239
и 0; после этого очередь `ods.event_dist` содержала 0 файлов на обеих нодах,
и ETL завершился успешно. Проверка `EXISTS` снова дала `1` на обеих нодах,
позиция равна `13`.
После пересборки `make check-services` дважды дал 5/1: только импорт Superset
не уложился в существующий тайм-аут 30 с. Диагностический пробник импорта с
ожиданием 90 с прошел 3/0. Финальный штатный повтор прошел 7/7 за 129 с.
**Перезамер 21 августа 2026 года при исполнении #124.** Образ Airflow собран с
`apache-airflow-providers-clickhousedb==1.0.0`,
`apache-airflow-providers-common-sql==2.0.1` и
`clickhouse-connect==1.6.0`; `pip check` не нашёл несовместимых зависимостей.
Адресная пересборка мира через `make rebuild-storage` заняла 3 м 5 с: все шесть
задач DDL, `pull_batch` и `parse_batch` завершились успешно. После неё прошли
смоук 21/21 за 17 с, проверка ClickHouse 10/10 и межсервисная проверка 7/7 за
100 с.
Первый `make up` после замены образа попал в короткий рассинхрон запуска
Airflow: API уже сообщил о здоровье и отдал `world_initialize` по GET, но POST
нового запуска ещё ответил `Dag with dag_id not found`. Ошибок импорта не было;
через несколько секунд повторный `make up` завершился успешно и не менял
готовый мир.
**Перезамер 21 августа 2026 года при исполнении #114.** После добавления
`kafka-exporter` и источника ClickHouse смоук прошёл 21/21 за 16–17 секунд,
`check-services` — 7/7 за 93 секунды. При остановленном экспортёре смоук дал
19/2: назвал нездоровый сервис и отсутствие одной из четырёх целей. После
возврата сервиса прогон снова дал 21/21.
После переноса #119 поверх этого состава повторный `make up` сохранил готовый
мир, смоук прошёл 21/21, проверка ClickHouse — 10/10, межсервисная — 7/7.
**Приёмка логики #119 до переноса поверх мониторинга 21 августа 2026 года.**
`make rebuild-storage` через Airflow прошёл за 3 м 9 с: позиция прошла задачи
`-1` → `0` → `8`, топики получили новые идентификаторы, группы дочитали новые
топики без отставания, а ODS принял 401 185 событий без брака и 2 851
актуальный заказ. Задача `build_generator_image` заняла по 1,5 с в двух
прогонах.
Повторный `make up` занял 2 м 41 с вместе со штатным повторным запуском служб
инициализации; `world_initialize` завершился за один цикл опроса и оставил
счётчики неизменными. Отдельно проверены красные отказы `world_initialize` при
`-1` и `world_next_day` при отсутствующей позиции, `0` и `-1`; `lifecycle`
отказано в чтении `system.users`, но разрешено чтение `system.settings`.
Итоговый холодный `make up` из пустых томов занял 9 м 39 с после 25 с на
`make clean`. Даг создал отсутствующую позицию и закончил на `8`; ODS снова
получил 401 185 событий без брака и 2 851 заказ. У `hits` два раздела, у
`orders` один, отставание обеих групп равно нулю. На тогдашнем составе стенда
прошли смоук 20/20, проверка ClickHouse 10/10 и межсервисная проверка 7/7.
Оборванную очистку проверили после записи `world_position=-1`: остановили
первую ноду ClickHouse, и `world_recreate` покраснел на удалении баз. Следующий
`make up` тоже покраснел и оставил `-1`. После возврата ноды повторный
`make rebuild-storage` восстановил мир.
**Прототип 21 августа 2026 года при исследовании #110.** Предшествующая
shell-версия узкой пересборки за 2 м 25 с пересоздала прикладные базы, отправила
401 185 событий и 6 656 версий заказов и вернула позицию с 11 на 8. Замер
оставлен как ориентир цены, но не подтверждает нынешнюю реализацию Airflow: тот
вариант не очищал Kafka и был отклонён как второй оркестратор мира.
**Перезамер 20 августа 2026 года при исполнении #96, до переезда в Airflow.**
`make up` после `make clean` прошёл за 8 м 18 с, повтор на живом стенде — за
3 м 35 с. Отправка семи слепков заняла около 28 секунд.
Прежний замер ниже — 2 м 50 с и 1 м 5 с до появления приёма заказов и дагов
модельного времени. Весь рост приписывать слепкам нельзя: на холодном прогоне
основное время ушло ещё до сценариев ожидания: `airflow-init` завершился
примерно на 320-й секунде, обработчик DAG стал здоров примерно на 458-й. Числа
здесь называют цену нынешнего стенда, а не выносят вердикт о регрессии.
**Перезамер 19 августа 2026 года при исполнении #95.** После добавления десятой
проверки словаря `make check-clickhouse` на чисто поднятом стенде прошёл за
24 секунды. Это цена всей цели, а не одной новой строки.
**Замер 9 августа 2026 года при появлении корневого `lint` (#65).** Три прогона
подряд — 0,39, 0,36 и 0,39 секунды на трёх файлах стенда; в таблице 0,4 с. На
машине с непрогретым кешем первый прогон дороже: `uvx` сначала скачивает ruff.
Тогда же снято, что двери не перекрываются: `ruff check --show-files` из корня
перечисляет ровно три файла стенда, из `generator/` — только файлы генератора.
Корневой конфиг правила генератору не подменяет: конфиг ищется вверх по дереву
от файла, и ближний выигрывает.
**Перезамер 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, начните с чтеца топика».
Затем положили в STG заведомо негодную строку — брак появился, и диагноз
сменился на второй, с признаком для различения: «сойдётся недостача с числом
брака — сломан разбор, не сойдётся — брак от прежних опытов». Строки опыта
убраны, день переигран `make generate-batch GENERATOR_DAY=2`; счёт вернулся к
401 185, и это заодно показало дедупликацию: повтор дня не удвоил счёт под
`FINAL`.
Замеры 6 августа 2026 года, стенд поднят заранее. Время взято по `time` и
совпадает с тем, что цель печатает сама. Оно плавает от прогона к прогону:
смоук дал 8 секунд дважды (без проверки Kafka, до её появления, — 5 и 6),
`check-services` — 43 и 44. В таблице стоит большее из замеренных.
Перемер 7 августа 2026 года — приёмка этапа 2 целиком: `make clean` и `make up`
подряд заняли 2 м 58 с и залили стартовый мир, дальше по порядку все три цели
на стенде и три без него. Смоук дал 9 секунд, `check-clickhouse` — 8,
`make test` — 71. `check-services` — 59 и 48; в таблицу по прежнему правилу
пошло большее. Против 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` с его минутой, хотя генератор пишет в 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 остался и по другому основанию —
их на машине не запускает никто.