- Зачем:
- находка «это стоило бы проверять регулярно» решалась заново в каждом
тикете и каждый раз тянулась в make check-clickhouse. У неё есть
назначенный дом: даги качества данных, которые придут со следующими
уровнями хранилища.
- Что:
- добавлен раздел «Корректность процессов живёт в дагах DQ, а не в целях
make»: цели make отвечают «стенд собран», свойства данных — работа дага.
- назван фильтр: про полноту дня, свежесть слоя, сходимость витрины с
источником — это даг, а не цель.
- названа учебная сторона: даг идёт по расписанию, пишет историю проверок
и разбирается как обычная задача Airflow — так качество данных устроено
в бою.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: parseDateTimeBestEffort на непонятной строке не краснеет, а достраивает
недостающее — обрезанное «20:00:21» становится первым января текущего года.
Такое сообщение проходило строгий приём с тихо неверным временем, то есть с
той самой порчей, ради которой класс key_field_unparsed и заведён.
Что:
- В обеих матвью разбор метки идёт parseDateTimeOrNull по формату
'%Y-%m-%dT%H:%i:%SZ'. Форма на проводе одна и каноническая, поэтому широта
best-effort не нужна вовсе, а платится за неё отключённой проверкой.
- Замеры в ADR 0005: три записи, которые best-effort достраивает; проверка,
что настройка cast_string_to_date_time_mode не спасает JSONExtract; сверка
на настоящих данных — по всем 101 252 строкам сырья модельного дня точный
формат разобрал метку у каждой и ни на одной не разошёлся с best-effort.
- Записано наблюдение стенда: пересозданная на живом чтеце матвью пропускает
ближайшее сообщение мимо ODS, через минуту то же сообщение разбирается.
Воспроизведено дважды; на нём я сам споткнулся при проверке этой правки.
Проверка: опыт строгого приёма прогнан заново — три сообщения дали событие и
два key_field_unparsed, включая обрезанную метку, которая раньше проходила
годной. DDL применяется на живом кластере.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: холодное ревью по двум линиям нашло дыру в следе опытов и три места,
где текст утверждает не то, что построено.
Что:
- Опыт «_load_ts переносится из сырья» прогнан и записан: у двух тысяч
событий метка совпала с меткой одной из доставок, случаев «метки нет среди
доставок» ноль. Туда же — ответ про форму ключа ODS: вопрос раздела 11
спеки закрывался молча.
- Дока хранилища говорила, что предикат собран из функций, не возвращающих
NULL; построено иначе — обнуляемый разбор есть, но кончается IS NOT NULL.
- Записана гарантия на JSONType: на не-JSON и пустой строке она отдаёт Null и
не бросает, то есть годится в предикат. Раньше первый класс брака стоял на
замере соседней функции.
- ttl_only_drop_parts у таблицы ошибок назван в доке хранилища.
- Комментарий матвью ужат: три вопроса строгого приёма пересказывали ADR 0005
целиком. Осталось то, чего по коду не видно, — запрет трогать arraySort и
замер про ISO-8601. Убрано неверное «в полусотне строк» и упоминание имени
таблицы хранилища в докстринге контракта генератора.
Проверка: DDL применяется на живом кластере; make lint, typecheck, docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: цепочка Kafka → STG → ODS достраивается последним этажом. Сырьё уже
доезжает (#37), настоящие события в топике есть (#41), а типизированного слоя
не было — событие негде было прочитать колонками, а брак негде увидеть.
Что:
- sql/ddl/20-ods-tables.sql — ods.event_rep/_dist на ReplacingMergeTree с
версией _load_ts, партиция по EventDate, ключ по разделу 1.3 спеки,
шардирование cityHash64(ClientID); ods.event_errors_rep/_dist с классом
брака, своими ключами и сроком жизни в месяц.
- sql/ddl/30-ods-views.sql — две матвью над stg.hits_raw_dist. Годность
считает предикат из трёх частей, вторая матвью берёт его дословное
отрицание, класс брака пишется первым совпавшим из трёх.
- Метку времени разбирает parseDateTimeBestEffortOrNull, а не JSONExtract:
ISO-8601 с суффиксом Z JSONExtract не берёт вовсе. Спека генератора
обещала обратное — обещание поправлено, форма на проводе не менялась.
- Сверка объявлений (contract-тест) снята из документов и из докстрингов
schema.py: сверх строгого приёма она ловила только смену типа.
- Документация приведена в соответствие: ADR 0005, дока хранилища и обе
спеки; группа «сказано по памяти» в доке хранилища опустела.
Проверка: make up && make check-clickhouse (8 проверок, 7,5 с); make lint,
make typecheck, make test (406), make docs без диффа. Разовые опыты при
исполнении — в теле PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: имя файла осталось от прежней цели smoke-cluster, которой больше нет,
и читатель ищет проверку ClickHouse не там, где она лежит.
Что: git mv scripts/clickhouse-smoke.sh scripts/check-clickhouse.sh, тем же
коммитом — вызов в Makefile и строка в карте целей. Абзац-объяснение в
docs/architecture/testing.md снят: он обещал переименование, которое здесь и
случилось.
Проверка: make check-clickhouse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- проверка, которая кормит стенд данными, оставляет их в мире менти
навсегда: у ODS срока хранения нет, и события, которых мир не рождал,
неотличимы в лабах от настоящих. Решение принято при разборе постановок
#41 и #43, чтобы оно решалось по карте, а не заново в каждом тикете.
- Что:
- добавлен раздел «Интеграционная проверка постоянной целью не становится»:
такой прогон делается один раз при исполнении, след — запись в теле PR.
- названо требование прибираться за собой и дешёвый способ это сделать:
модельный день за границей оси мира и снос его партиции.
- названо, что стережёт цепочку постоянно вместо неё — проверки на
настоящих данных, которые стенд произвёл сам.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- проверка была сломана с рождения цели: bash -n со списком файлов
разбирает только первый, остальные уходят ему в аргументы. Из пяти
скриптов проверялся один, и за всё время этого никто не заметил.
- чинить незачем: скрипты стенда запускают с той же машины, и
синтаксическая ошибка вылезает при первом же запуске с номером строки.
Учебной ценности в проверке нет — из неё не узнаёшь ничего, кроме того,
что у bash есть ключ -n.
- держалась она не строчкой, а двенадцатью: обход репозитория, временный
файл со списком, mapfile и две ветки на пустой список.
- Что:
- из scripts/config-test.sh убраны разбор Bash и весь аппарат сбора
списка файлов; 53 строки стали 40.
- разбор файлов DAG остался и получил комментарий с основанием: их на
машине не запускает никто, обработчик разбирает их внутри контейнера, и
ошибка всплывает не сообщением, а молча пропавшим DAG.
- README и карта проверок больше не обещают проверку синтаксиса Bash.
- в карте записано, почему проверку не стоит заводить заново.
- Проверка:
- make config-test зелёный.
- оставшийся разбор DAG краснеет: незакрытая скобка в dags/test_kafka.py
роняет цель с SyntaxError и ненулевым кодом; файл восстановлен.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- деление целей оставило дыру: про Airflow, Superset, Prometheus и Grafana
смоук стучится с машины в отображённый порт, а про Kafka после переезда
check_kafka_from_host знал только «контейнер здоров».
- вердикт этот приходит из healthcheck в compose.yaml, а тот спрашивает
брокер изнутри и по внутреннему слушателю: объявленный наружу адрес может
вести не туда, и Kafka всё равно останется здоровой.
- поломка популярная и показательная: клиент подключается, получает
метаданные и молча виснет на адресе, которого с его стороны нет. Менти
узнаёт, что у брокера два слушателя и зачем нужен advertised.listeners.
Генератор будет писать в Kafka именно с машины.
- Что:
- check_kafka_external_listener в make smoke: запрос списка топиков с машины
через отображённый порт, ответ приходит только если объявленный адрес ведёт
туда же. Комментарий у проверки объясняет, от чего она заведена.
- ожидание ответа ограничено 15 секундами при замеренных 2,6 — впятеро
больше, чем стоит зелёный прогон.
- README и карта проверок: новая проверка названа, доводы записаны, цена
смоука обновлена с 6 до 8 секунд.
- Проверка:
- make config-test, make smoke (20 проверок, 8 с), make check-clickhouse,
make check-services — зелёные.
- краснеет на своей поломке: брокеру объявлен адрес kafka-nowhere:29092 при
целом внутреннем слушателе — проверка состояния контейнера осталась
зелёной, смоук покраснел именно на этой строке. После проверки Kafka
возвращена в исходное состояние, посторонних контейнеров не осталось.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- смоук перестал быть быстрым: 42 секунды из 48 съедали шесть проверок,
которые ждут службу — запуск DAG, вход в Superset, Kafka с машины.
- имена целей врали: смоуком звались и глубокая проверка кластера, и
интеграционные проверки; префикс достался им от общего происхождения.
- нигде не было записано, зачем в репозитории каждая цель и куда класть
новую проверку, — без записи скрипт дорастёт снова.
- Что:
- ось деления — кого спрашивают, а не сколько стоит: make smoke (стенд
собран), make check-clickhouse (спрашивают у ClickHouse), новая
make check-services (службы работают).
- шесть тяжёлых проверок переехали в scripts/stand-services.sh; общее —
счёт, обращение к Compose, зависимости машины и check_containers_survived
— вынесено в scripts/stand-common.sh, копипасты нет.
- smoke-cluster переименована в check-clickhouse; имя файла скрипта не
тронуто (в него встраивается проверка договора со схемой), расхождение
названо в карте.
- смоук и check-services печатают своё время в строке ИТОГ; порога по
времени нет — по доводу ADR 0004.
- docs/architecture/testing.md: карта всех семи целей, правило быстрого
смоука словами, лесенка по частоте и правило про краснеющую проверку,
переехавшее из README; указатель из AGENTS.md.
- README: описания целей сокращены, карта не дублируется; быстрый старт
показывает работающий стенд, а не только собранный.
- планка приёмки этапа в спеке названа поимённо: три цели вместо
«smoke-проверки».
- Проверка:
- make config-test, make smoke (19 проверок, 6 с), make check-clickhouse
(8 проверок, 7 с), make check-services (7 проверок, 44 с) — зелёные.
- 19 + 7 = 25 разных проверок, как и до деления: check_containers_survived
считается дважды намеренно.
- краснеют обе разделённые цели: со снятым prometheus смоук дал три ошибки,
с подменённым UUID подключения Superset покраснел check-services.
Co-Authored-By: Claude Opus 5 (1M context) <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>
- Зачем:
- холодное ревью связности нашло девять мест, где вставленный текст спорит с
соседним; отдельно вскрылось, что представление дат в JSON не зафиксировано
нигде, а #43 обязан его знать раньше, чем #41 напишет сериализатор.
- Что:
- гарантия приёма переписана: после снятия синхронной вставки «хотя бы один
раз» стало неправдой — есть и окно потери, и окно дубля.
- критерий выбора пяти опорных колонок приведён к списку, который он
порождает; `CounterID` оговорён отдельно.
- «переобработки у ODS нет вовсе» смягчено до пакетной: ручная вставка из
сырья в пределах окна возможна.
- в спеку генератора добавлена форма дат на проводе — ISO-8601, с доводом от
читаемости слоя сырья.
- убраны осиротевшая фраза про порядок сервисов, дубль порядка классов брака,
устаревшая датировка сверки и ещё три следа вставок.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- три холодных ревью и сверка с документацией ClickHouse нашли противоречия
между докой, ADR и спекой: исполнитель #37 получал два разных ответа на
один вопрос, а два утверждения о движке оказались неверными.
- Что:
- раскладка файлов DDL перестроена — сначала таблицы, матвью приёма
последней: иначе часть событий тихо минует ODS.
- синхронная вставка снята с пути приёма: настройка недостижима для потока
Kafka-движка и связывает шарды; на ETL-вставках осталась.
- у таблицы ошибок появился класс брака с порядком проверки, у сырья и
ошибок названы движки и ключи сортировки.
- в доку добавлен раздел «Что проверено»: сверенное с документацией,
проверяемое на стенде и сказанное по памяти разведены.
- в спеке выправлены источник матвью разбора, пять опорных колонок, имена
четырёх витрин и ссылка на несуществующую цель make.
- Проверка:
- make config-test
- grep по устаревшим именам файлов DDL и витрин — пусто
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- тикет #37 молча опирался на конвенции хранилища, которых в проекте не
было; без них #43 и следующие этапы разъехались бы в именах, служебных
колонках и механике приёма.
- Что:
- ADR 0005: топик читается байтами в STG, разбор идёт функциями в матвью
ODS; строгий приём — сверка набора ключей плюс Nullable на пяти опорных
колонках.
- ADR 0006: суффикс вида в именах объектов (_rep, _dist, _kafka, _mv, _v).
- docs/architecture/storage.md: конвенции имён и служебных колонок, путь в
keeper, раскладка по шардам, срок жизни сырья, свойства приёма, раскладка
файлов DDL и карта таблиц.
- спеки приведены в соответствие: механизм строгого приёма, имена объектов,
контракт транспорта «одно событие — одно сообщение Kafka», три проверки
при исполнении.
- Проверка:
- make config-test