DDL-бутстрап, топик hits и приём сырья в STG обеими нодами #37

Closed
opened 2026-08-01 21:19:37 +03:00 by ddmitry · 4 comments
Owner

Part of #4.

Цель

Дать стенду руки для DDL и довести сырьё до STG: механизм применения DDL
при make up, топик hits с двумя партициями и приём сырых событий
обеими нодами. Типизированный ODS со строгим приёмом — следующий тикет
(#43): этот закладывает фундамент, на котором тот строится.

Развилки закрыты грилингом и холодным ревью до начала работы — см. «Решено
до начала».

Первым делом: четыре опыта

Дока хранилища в разделе «Что проверено» держит группу «сказано по памяти».
Четыре из пяти утверждений закрывает этот тикет — они дешёвые, а от двух
зависит DDL, который иначе придётся переписывать.

Черновой связки (чтец + матвью + таблица-цель) для этого хватит; «до написания
DDL» здесь означает «до боевых файлов в sql/ddl/», а не «вообще без SQL».

  1. RawBLOB даёт ровно одну строку на сообщение. Три сообщения в топик,
    SELECT count(). Документация описывает формат как «читает вход в одно
    значение» — про файл понятно, про пачку сообщений из топика нет. Склеит —
    переделывать придётся решение целиком, запасной формат LineAsString
    (режет по переводу строки, события у нас однострочные).
  2. Форма виртуальной колонки _timestamp — обнуляемость и разрядность. От
    неё зависит, чем объявлять kafka_timestamp.
  3. DEFAULT hostName() вычисляется на шарде-получателе, а не на
    вставляющей ноде. Ради этого служебные колонки и заполняются выражением в
    SELECT матвью; если опыт покажет обратное, довод в доке надо переписать.
  4. DROP/REPLACE PARTITION не работает по Distributed. Прямого запрета в
    документации нет, все примеры даны для семейства MergeTree.

Пятое утверждение группы — что упавшая матвью останавливает потребление —
относится к #43, там и проверяется.

Результаты записать в доку хранилища: каждое утверждение либо переезжает в
группу «сверено», либо получает запись, что опыт показал иначе.

Что войдёт

  • Механизм применения DDL при make up: два одноразовых сервиса по образцу
    airflow-init и superset-init. Сначала kafka-init создаёт топик, затем
    clickhouse-init применяет sql/ddl/*.sql по порядку имён с ноды 1,
    ON CLUSTER. Идемпотентность — через CREATE ... IF NOT EXISTS.
  • Образцы копируются не целиком, и в двух местах. clickhouse-init обязан
    ждать готовности обеих нод: ON CLUSTER ждёт все хосты и бросает по
    таймауту, а airflow-init ждёт только первую ноду, superset-init — только
    вторую. И оба образца переживают make up --wait лишь потому, что от них
    зависят долгоживущие сервисы; у новой пары таких зависимых нет — проверить
    поведение и решить.
  • Создание топика hits с 2 партициями — явно, не автосозданием:
    KAFKA_NUM_PARTITIONS: "1" в compose иначе молча даст одну партицию, и урок
    «обе ноды читают топик» умрёт.
  • Базы stg и ods ON CLUSTER. Имя кластера — clickstream_cluster
    (infra/clickhouse/config.d/cluster.xml).
  • stg.hits_raw_kafka — чтец на обеих нодах, группа потребителей
    clickstream_hits, формат RawBLOB. Колонка ровно одна, raw: формат
    рассчитан на таблицу с единственным полем String, метаданные берутся только
    из виртуальных колонок, добавить к чтецу своё нельзя.
  • stg.hits_raw_rep / stg.hits_raw_dist — сырьё. Колонки: raw;
    kafka_topic и consumer_hostLowCardinality(String); kafka_partition
    и kafka_offsetUInt64; kafka_timestamp — по итогу опыта 2;
    _load_tsDateTime64(3).
    Движок — ReplicatedMergeTree, именно не Replacing: повтор доставки в
    сырье обязан быть виден. ORDER BY (kafka_partition, kafka_offset),
    PARTITION BY toDate(_load_ts), TTL трое суток, ttl_only_drop_parts = 1,
    шардирование cityHash64(raw), путь в keeper
    /clickhouse/tables/{shard}/{database}/{table}.
  • stg.hits_raw_mv — наполняет сырьё из чтеца, пишет в _dist, а не в
    локальную таблицу. Служебные колонки заполняются выражением в SELECT, а не
    DEFAULT в таблице.
  • Раскладка файлов — 00-databases.sql, 10-stg-tables.sql,
    40-stg-views.sql. Пропуск в нумерации намеренный: 20-ods-tables.sql и
    30-ods-views.sql приносит #43, а матвью приёма обязана создаваться после
    матвью разбора. Kafka-движок начинает читать топик в момент её создания, и
    всё, что доедет до появления разбора, ляжет в сырьё и не попадёт в ODS
    никуда — ни в событие, ни в ошибки.

Синхронной вставки на пути приёма нет: distributed_foreground_insert
недостижим для фонового потока Kafka-движка и связывает шарды. Довод целиком —
в доке хранилища, раздел «Приём».

Новые подстановки ${...} в compose.yaml записывать в .env.example:
их сверяет check_env_consistency в scripts/stand-smoke.sh, иначе красит
make smoke.

Решено до начала

  • Форма приёма и формат чтеца — docs/adr/0005-event-ingestion.md. Оттуда же
    снято прежнее ограничение тикета «ошибки разбора должны рождаться на шаге
    Kafka-движка»: критерий #43 достигается средствами матвью.
  • Имена объектов — docs/adr/0006-object-naming.md.
  • Конвенции служебных колонок, путь в keeper, срок жизни сырья, раскладка
    файлов DDL, свойства приёма — docs/architecture/storage.md.

Критерии приёмки

  • Четыре опыта из «первым делом» прогнаны, результаты записаны в доку
    хранилища: раздел «Что проверено» перестроен по факту, а не переписан
    на глаз.
  • make clean && make up с нуля поднимает стенд и применяет DDL;
    повторный make up поверх живого тома тоже зелёный.
  • Топик hits существует с 2 партициями (kafka-topics.sh --describe
    из контейнера Kafka).
  • Тестовое сообщение, отправленное в hits, появляется в
    stg.hits_raw_dist с метаданными доставки.
  • Обе партиции топика и обе ноды видны в stg.hits_raw_dist. Отправлять
    с ключами: без ключа sticky-партиционер сложит всю пачку в одну
    партицию, и критерий станет лотереей. Подобрать пару ключей, дающих
    разные партиции, и приложить их к проверке.
  • Сообщение, не являющееся JSON, приём не останавливает и лежит в сырье
    как есть.

Ожидание доезда — опросом с таймаутом по образцу wait_for_local_table из
tests/smoke-guards.sh, а не sleep наугад: умолчание
stream_flush_interval_ms — 7,5 с, и фиксированная пауза даёт шаткую проверку.

Границы

  • ods.event, строгий приём и contract-тест — #43.
  • Таблицы заказов (топик orders) — этап 3.
  • Генератор не трогать.

Сначала прочитать

  • docs/adr/0005-event-ingestion.md и docs/adr/0006-object-naming.md.
  • docs/architecture/storage.md — конвенции, раскладка DDL, свойства приёма и
    раздел «Что проверено».
  • docs/specs/2026-07-30-stand-v2-realism.md — разделы 1.3, 6, 7, 11.
  • compose.yaml, Makefile, infra/clickhouse/config.d/ — что реально есть
    после этапа 1.
  • tests/smoke-guards.sh — образец ожидания и заведённая практика сторожей.

Проверка

  • make clean && make up && make up — второй запуск проверяет идемпотентность
  • ручная отправка тестовых сообщений с ключами в hits (console producer из
    контейнера Kafka), SELECT из stg.hits_raw_dist
  • отдельно — отправка строки, не являющейся JSON: приём должен выжить
Part of #4. ## Цель Дать стенду руки для DDL и довести сырьё до STG: механизм применения DDL при `make up`, топик `hits` с двумя партициями и приём сырых событий обеими нодами. Типизированный ODS со строгим приёмом — следующий тикет (#43): этот закладывает фундамент, на котором тот строится. Развилки закрыты грилингом и холодным ревью до начала работы — см. «Решено до начала». ## Первым делом: четыре опыта Дока хранилища в разделе «Что проверено» держит группу «сказано по памяти». Четыре из пяти утверждений закрывает этот тикет — они дешёвые, а от двух зависит DDL, который иначе придётся переписывать. Черновой связки (чтец + матвью + таблица-цель) для этого хватит; «до написания DDL» здесь означает «до боевых файлов в `sql/ddl/`», а не «вообще без SQL». 1. **`RawBLOB` даёт ровно одну строку на сообщение.** Три сообщения в топик, `SELECT count()`. Документация описывает формат как «читает вход в одно значение» — про файл понятно, про пачку сообщений из топика нет. Склеит — переделывать придётся решение целиком, запасной формат `LineAsString` (режет по переводу строки, события у нас однострочные). 2. **Форма виртуальной колонки `_timestamp`** — обнуляемость и разрядность. От неё зависит, чем объявлять `kafka_timestamp`. 3. **`DEFAULT hostName()` вычисляется на шарде-получателе**, а не на вставляющей ноде. Ради этого служебные колонки и заполняются выражением в `SELECT` матвью; если опыт покажет обратное, довод в доке надо переписать. 4. **`DROP/REPLACE PARTITION` не работает по `Distributed`.** Прямого запрета в документации нет, все примеры даны для семейства MergeTree. Пятое утверждение группы — что упавшая матвью останавливает потребление — относится к #43, там и проверяется. Результаты записать в доку хранилища: каждое утверждение либо переезжает в группу «сверено», либо получает запись, что опыт показал иначе. ## Что войдёт - Механизм применения DDL при `make up`: два одноразовых сервиса по образцу `airflow-init` и `superset-init`. Сначала `kafka-init` создаёт топик, затем `clickhouse-init` применяет `sql/ddl/*.sql` по порядку имён с ноды 1, `ON CLUSTER`. Идемпотентность — через `CREATE ... IF NOT EXISTS`. - Образцы копируются не целиком, и в двух местах. `clickhouse-init` обязан ждать готовности **обеих** нод: `ON CLUSTER` ждёт все хосты и бросает по таймауту, а `airflow-init` ждёт только первую ноду, `superset-init` — только вторую. И оба образца переживают `make up --wait` лишь потому, что от них зависят долгоживущие сервисы; у новой пары таких зависимых нет — проверить поведение и решить. - Создание топика `hits` с 2 партициями — явно, не автосозданием: `KAFKA_NUM_PARTITIONS: "1"` в compose иначе молча даст одну партицию, и урок «обе ноды читают топик» умрёт. - Базы `stg` и `ods` ON CLUSTER. Имя кластера — `clickstream_cluster` (`infra/clickhouse/config.d/cluster.xml`). - `stg.hits_raw_kafka` — чтец на обеих нодах, группа потребителей `clickstream_hits`, формат `RawBLOB`. Колонка ровно одна, `raw`: формат рассчитан на таблицу с единственным полем `String`, метаданные берутся только из виртуальных колонок, добавить к чтецу своё нельзя. - `stg.hits_raw_rep` / `stg.hits_raw_dist` — сырьё. Колонки: `raw`; `kafka_topic` и `consumer_host` — `LowCardinality(String)`; `kafka_partition` и `kafka_offset` — `UInt64`; `kafka_timestamp` — по итогу опыта 2; `_load_ts` — `DateTime64(3)`. Движок — `ReplicatedMergeTree`, именно не `Replacing`: повтор доставки в сырье обязан быть виден. `ORDER BY (kafka_partition, kafka_offset)`, `PARTITION BY toDate(_load_ts)`, TTL трое суток, `ttl_only_drop_parts = 1`, шардирование `cityHash64(raw)`, путь в keeper `/clickhouse/tables/{shard}/{database}/{table}`. - `stg.hits_raw_mv` — наполняет сырьё из чтеца, пишет в `_dist`, а не в локальную таблицу. Служебные колонки заполняются выражением в `SELECT`, а не `DEFAULT` в таблице. - Раскладка файлов — `00-databases.sql`, `10-stg-tables.sql`, `40-stg-views.sql`. Пропуск в нумерации намеренный: `20-ods-tables.sql` и `30-ods-views.sql` приносит #43, а матвью приёма обязана создаваться после матвью разбора. Kafka-движок начинает читать топик в момент её создания, и всё, что доедет до появления разбора, ляжет в сырьё и не попадёт в ODS никуда — ни в событие, ни в ошибки. Синхронной вставки на пути приёма нет: `distributed_foreground_insert` недостижим для фонового потока Kafka-движка и связывает шарды. Довод целиком — в доке хранилища, раздел «Приём». Новые подстановки `${...}` в `compose.yaml` записывать в `.env.example`: их сверяет `check_env_consistency` в `scripts/stand-smoke.sh`, иначе красит `make smoke`. ## Решено до начала - Форма приёма и формат чтеца — docs/adr/0005-event-ingestion.md. Оттуда же снято прежнее ограничение тикета «ошибки разбора должны рождаться на шаге Kafka-движка»: критерий #43 достигается средствами матвью. - Имена объектов — docs/adr/0006-object-naming.md. - Конвенции служебных колонок, путь в keeper, срок жизни сырья, раскладка файлов DDL, свойства приёма — docs/architecture/storage.md. ## Критерии приёмки - [x] Четыре опыта из «первым делом» прогнаны, результаты записаны в доку хранилища: раздел «Что проверено» перестроен по факту, а не переписан на глаз. - [x] `make clean && make up` с нуля поднимает стенд и применяет DDL; повторный `make up` поверх живого тома тоже зелёный. - [x] Топик `hits` существует с 2 партициями (`kafka-topics.sh --describe` из контейнера Kafka). - [x] Тестовое сообщение, отправленное в `hits`, появляется в `stg.hits_raw_dist` с метаданными доставки. - [x] Обе партиции топика и обе ноды видны в `stg.hits_raw_dist`. Отправлять **с ключами**: без ключа sticky-партиционер сложит всю пачку в одну партицию, и критерий станет лотереей. Подобрать пару ключей, дающих разные партиции, и приложить их к проверке. - [x] Сообщение, не являющееся JSON, приём не останавливает и лежит в сырье как есть. Ожидание доезда — опросом с таймаутом по образцу `wait_for_local_table` из `tests/smoke-guards.sh`, а не `sleep` наугад: умолчание `stream_flush_interval_ms` — 7,5 с, и фиксированная пауза даёт шаткую проверку. ## Границы - `ods.event`, строгий приём и contract-тест — #43. - Таблицы заказов (топик `orders`) — этап 3. - Генератор не трогать. ## Сначала прочитать - docs/adr/0005-event-ingestion.md и docs/adr/0006-object-naming.md. - docs/architecture/storage.md — конвенции, раскладка DDL, свойства приёма и раздел «Что проверено». - docs/specs/2026-07-30-stand-v2-realism.md — разделы 1.3, 6, 7, 11. - compose.yaml, Makefile, infra/clickhouse/config.d/ — что реально есть после этапа 1. - tests/smoke-guards.sh — образец ожидания и заведённая практика сторожей. ## Проверка - `make clean && make up && make up` — второй запуск проверяет идемпотентность - ручная отправка тестовых сообщений с ключами в `hits` (console producer из контейнера Kafka), SELECT из `stg.hits_raw_dist` - отдельно — отправка строки, не являющейся JSON: приём должен выжить
ddmitry added the ready-for-agent label 2026-08-01 21:19:58 +03:00
ddmitry changed title from DDL событий ON CLUSTER и приём hits обеими нодами to DDL-бутстрап, топик hits и приём сырья в STG обеими нодами 2026-08-01 22:04:17 +03:00
Author
Owner

Постановка переписана после грилинга и двух холодных ревью (2026-08-03/04).

Что изменилось по существу:
— форма приёма решена: чтец читает топик формата RawBLOB, разбора на входе нет (ADR 0005). Прежнее ограничение тикета «ошибки разбора должны рождаться на шаге Kafka-движка» снято: критерий #43 достигается средствами матвью;
— имена объектов переехали на суффикс вида (ADR 0006), поэтому в критериях теперь stg.hits_raw_dist и consumer_host;
— путь в keeper решён: /clickhouse/tables/{shard}/{database}/{table}, без {uuid};
— извлечённый event_date из сырья убран: модельный день — свойство содержимого, он живёт в ODS ключом партиции;
— добавлены нарезка по дню загрузки, TTL трое суток и синхронная вставка в Distributed;
— два новых критерия: не-JSON не останавливает приём; RawBLOB даёт ровно одну строку на сообщение.

Конвенции и доводы — docs/architecture/storage.md.

Постановка переписана после грилинга и двух холодных ревью (2026-08-03/04). Что изменилось по существу: — форма приёма решена: чтец читает топик формата RawBLOB, разбора на входе нет (ADR 0005). Прежнее ограничение тикета «ошибки разбора должны рождаться на шаге Kafka-движка» снято: критерий #43 достигается средствами матвью; — имена объектов переехали на суффикс вида (ADR 0006), поэтому в критериях теперь stg.hits_raw_dist и consumer_host; — путь в keeper решён: /clickhouse/tables/{shard}/{database}/{table}, без {uuid}; — извлечённый event_date из сырья убран: модельный день — свойство содержимого, он живёт в ODS ключом партиции; — добавлены нарезка по дню загрузки, TTL трое суток и синхронная вставка в Distributed; — два новых критерия: не-JSON не останавливает приём; RawBLOB даёт ровно одну строку на сообщение. Конвенции и доводы — docs/architecture/storage.md.
Author
Owner

Переписано после холодного ревью в три линзы и сверки утверждений о ClickHouse
с документацией. Против прежней постановки изменилось вот что.

Раскладка файлов DDL другая. Было чередование по слоям (10-stg-tables,
11-stg-views, 20-ods-tables, 21-ods-views). Оно включало чтение топика
раньше, чем появлялся разбор: всё доехавшее в зазоре легло бы в сырьё и молча
миновало ODS. Стало — сначала все таблицы, матвью приёма последней,
40-stg-views.sql.

Синхронная вставка снята. distributed_foreground_insert = 1 недостижим
для фонового потока Kafka-движка (только профилем пользователя в конфигурации
ноды) и вдобавок связывает шарды: упала вторая нода — приём встаёт целиком, а
не наполовину. Окно потери принято осознанно, довод целиком — в доке хранилища.

Появился шаг «первым делом» — до написания DDL проверить, что RawBLOB
даёт ровно одну строку на сообщение. Документация этот случай не описывает, а
если сообщения склеятся, переделывать придётся решение, а не запрос.

Названы типы служебных колонок, движок и ключ сортировки сырья. Движок
именно ReplicatedMergeTree: Replacing отменил бы свойство слоя, ради
которого он заведён.

Оговорка про образцы. clickhouse-init обязан ждать обе ноды, тогда как
airflow-init ждёт первую, а superset-init — вторую; и поведение --wait у
одноразового сервиса без зависимых остаётся открытым вопросом.

Переписано после холодного ревью в три линзы и сверки утверждений о ClickHouse с документацией. Против прежней постановки изменилось вот что. **Раскладка файлов DDL другая.** Было чередование по слоям (`10-stg-tables`, `11-stg-views`, `20-ods-tables`, `21-ods-views`). Оно включало чтение топика раньше, чем появлялся разбор: всё доехавшее в зазоре легло бы в сырьё и молча миновало ODS. Стало — сначала все таблицы, матвью приёма последней, `40-stg-views.sql`. **Синхронная вставка снята.** `distributed_foreground_insert = 1` недостижим для фонового потока Kafka-движка (только профилем пользователя в конфигурации ноды) и вдобавок связывает шарды: упала вторая нода — приём встаёт целиком, а не наполовину. Окно потери принято осознанно, довод целиком — в доке хранилища. **Появился шаг «первым делом»** — до написания DDL проверить, что `RawBLOB` даёт ровно одну строку на сообщение. Документация этот случай не описывает, а если сообщения склеятся, переделывать придётся решение, а не запрос. **Названы типы служебных колонок, движок и ключ сортировки сырья.** Движок именно `ReplicatedMergeTree`: `Replacing` отменил бы свойство слоя, ради которого он заведён. **Оговорка про образцы.** `clickhouse-init` обязан ждать обе ноды, тогда как `airflow-init` ждёт первую, а `superset-init` — вторую; и поведение `--wait` у одноразового сервиса без зависимых остаётся открытым вопросом.
Author
Owner

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

Четыре опыта поручены явно. Дока хранилища обещает, что утверждения из
группы «сказано по памяти» проверяются заодно с этим тикетом, а тикет о них
молчал. Теперь они перечислены с доводом, зачем каждый: от двух зависит DDL.
Пятое утверждение группы — про упавшую матвью — переехало в #43, где ему место.

Критерий про обе ноды перестал быть лотереей. Отправка без ключей кладёт
всю пачку в одну партицию sticky-партиционером: в сырье была бы одна партиция и
один consumer_host, и критерий зеленел или краснел по удаче. Теперь требуются
ключи, дающие разные партиции.

Ожидание доезда — опросом по образцу wait_for_local_table из
tests/smoke-guards.sh, а не sleep наугад: умолчание
stream_flush_interval_ms — 7,5 секунды.

«До написания DDL» уточнено: опыты требуют черновой связки чтец + матвью +
цель, речь про боевые файлы в sql/ddl/, а не про полный отказ от SQL.

Плюс напоминание про .env.example: новые подстановки в compose.yaml без
записи туда красят make smoke.

Вторая правка за день, по итогам холодного ревью тикетов как наряда на работу. Вопрос ревьюеру ставился один: сможет ли исполнитель сделать задачу, имея только тикет и названные в нём документы, ни разу не спросив автора. **Четыре опыта поручены явно.** Дока хранилища обещает, что утверждения из группы «сказано по памяти» проверяются заодно с этим тикетом, а тикет о них молчал. Теперь они перечислены с доводом, зачем каждый: от двух зависит DDL. Пятое утверждение группы — про упавшую матвью — переехало в #43, где ему место. **Критерий про обе ноды перестал быть лотереей.** Отправка без ключей кладёт всю пачку в одну партицию sticky-партиционером: в сырье была бы одна партиция и один `consumer_host`, и критерий зеленел или краснел по удаче. Теперь требуются ключи, дающие разные партиции. **Ожидание доезда** — опросом по образцу `wait_for_local_table` из `tests/smoke-guards.sh`, а не `sleep` наугад: умолчание `stream_flush_interval_ms` — 7,5 секунды. **«До написания DDL» уточнено**: опыты требуют черновой связки чтец + матвью + цель, речь про боевые файлы в `sql/ddl/`, а не про полный отказ от SQL. Плюс напоминание про `.env.example`: новые подстановки в `compose.yaml` без записи туда красят `make smoke`.
Author
Owner

Работа сделана, коммит daf1338 в ветке feat/37-ddl-bootstrap-stg-ingest.
Все шесть критериев приёмки прогнаны на стенде с чистого тома 6 августа
2026 года; чекбоксы проставлены по факту прогона.

Четыре опыта — все подтвердили ожидание. Результаты переехали в
docs/architecture/storage.md, раздел «Что проверено», новая группа
«Проверено на стенде». Из группы «сказано по памяти» осталось два
утверждения, оба ждут матвью разбора из #43.

Три решения сверх буквы тикета — приняты владельцем по ходу:

  1. kafka_timestampNullable(DateTime64(3)) из виртуальной
    _timestamp_ms. Тикет оставлял форму на «итог опыта 2»; опыт показал,
    что миллисекунды доступны, и терять их незачем.
  2. hostname: у обеих нод ClickHouse в compose.yaml — сверх состава
    тикета. Без него hostName() отдаёт идентификатор контейнера, и
    колонка consumer_host теряет учебный смысл.
  3. Поведение make up --wait с одноразовыми службами решено замером, а не
    рассуждением: на Compose 2.40.3 служба без зависимых считается упавшей
    даже при выходе с нулём. Отсюда цепочка
    kafka-initclickhouse-initairflow-init. Замер с датой и
    версией — в доке хранилища.

Найденная граница приёма. RawBLOB молча теряет запись с пустым
значением и запись-надгробие: офсет потребителя двигается, строки нет ни в
сырье, ни в ошибках, приём не останавливается. Запасной LineAsString
теряет их точно так же — формат тут не рычаг. Принято как известное
свойство, отдельного тикета не заводим; практический признак — дыра в
kafka_offset. Замер и довод — в доке хранилища, оговорка продублирована
в комментарии sql/ddl/10-stg-tables.sql и в ADR 0005.

Открытый хвост. Сценарий приёмки, которым прогонялись критерии, в
репозиторий не внесён: решение «делать ли его постоянным сторожем в
tests/» намеренно оставлено владельцу.

Работа сделана, коммит `daf1338` в ветке `feat/37-ddl-bootstrap-stg-ingest`. Все шесть критериев приёмки прогнаны на стенде с чистого тома 6 августа 2026 года; чекбоксы проставлены по факту прогона. **Четыре опыта — все подтвердили ожидание.** Результаты переехали в `docs/architecture/storage.md`, раздел «Что проверено», новая группа «Проверено на стенде». Из группы «сказано по памяти» осталось два утверждения, оба ждут матвью разбора из #43. **Три решения сверх буквы тикета** — приняты владельцем по ходу: 1. `kafka_timestamp` — `Nullable(DateTime64(3))` из виртуальной `_timestamp_ms`. Тикет оставлял форму на «итог опыта 2»; опыт показал, что миллисекунды доступны, и терять их незачем. 2. `hostname:` у обеих нод ClickHouse в `compose.yaml` — сверх состава тикета. Без него `hostName()` отдаёт идентификатор контейнера, и колонка `consumer_host` теряет учебный смысл. 3. Поведение `make up --wait` с одноразовыми службами решено замером, а не рассуждением: на Compose 2.40.3 служба без зависимых считается упавшей даже при выходе с нулём. Отсюда цепочка `kafka-init` → `clickhouse-init` → `airflow-init`. Замер с датой и версией — в доке хранилища. **Найденная граница приёма.** `RawBLOB` молча теряет запись с пустым значением и запись-надгробие: офсет потребителя двигается, строки нет ни в сырье, ни в ошибках, приём не останавливается. Запасной `LineAsString` теряет их точно так же — формат тут не рычаг. Принято как известное свойство, отдельного тикета не заводим; практический признак — дыра в `kafka_offset`. Замер и довод — в доке хранилища, оговорка продублирована в комментарии `sql/ddl/10-stg-tables.sql` и в ADR 0005. **Открытый хвост.** Сценарий приёмки, которым прогонялись критерии, в репозиторий не внесён: решение «делать ли его постоянным сторожем в `tests/`» намеренно оставлено владельцу.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Reference: ddmitry/clickstream-data-platform#37