docs(storage): конвенции и приём событий выправлены после ревью
- Зачем:
- три холодных ревью и сверка с документацией ClickHouse нашли противоречия
между докой, ADR и спекой: исполнитель #37 получал два разных ответа на
один вопрос, а два утверждения о движке оказались неверными.
- Что:
- раскладка файлов DDL перестроена — сначала таблицы, матвью приёма
последней: иначе часть событий тихо минует ODS.
- синхронная вставка снята с пути приёма: настройка недостижима для потока
Kafka-движка и связывает шарды; на ETL-вставках осталась.
- у таблицы ошибок появился класс брака с порядком проверки, у сырья и
ошибок названы движки и ключи сортировки.
- в доку добавлен раздел «Что проверено»: сверенное с документацией,
проверяемое на стенде и сказанное по памяти разведены.
- в спеке выправлены источник матвью разбора, пять опорных колонок, имена
четырёх витрин и ссылка на несуществующую цель make.
- Проверка:
- make config-test
- grep по устаревшим именам файлов DDL и витрин — пусто
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,12 @@
|
||||
## Решение
|
||||
|
||||
Топик `hits` читает одна Kafka-таблица формата `RawBLOB`: сообщение ложится в
|
||||
`stg.hits_raw_rep` строкой, как пришло, рядом с метаданными доставки. Ни
|
||||
`stg.hits_raw_dist` строкой, как пришло, рядом с метаданными доставки. Ни
|
||||
типизации, ни проверки на этом шаге нет — слой сырья ничего не интерпретирует.
|
||||
У формата есть следствие для DDL: он читает вход в одно значение и рассчитан на
|
||||
таблицу с единственной колонкой, поэтому у чтеца она ровно одна — `raw`, а
|
||||
метаданные доставки берутся только из виртуальных колонок и добавить к чтецу
|
||||
что-либо своё нельзя.
|
||||
|
||||
Типизированный слой наполняют две матвью, привязанные к `stg.hits_raw_dist`.
|
||||
Поля достаются `JSONExtract`. В таблицу ошибок уходят три класса брака:
|
||||
@@ -16,8 +20,17 @@
|
||||
на валидность: `isValidJSON('123')` возвращает единицу, скаляр — тоже законный
|
||||
JSON.
|
||||
|
||||
Классы пересекаются: скаляр проваливает заодно и сверку ключей, потому что
|
||||
`JSONExtractKeys` от него даёт пустой массив. Поэтому они проверяются по порядку,
|
||||
а в колонку `error_class` пишется первый совпавший — `not_an_object`,
|
||||
`keyset_mismatch`, `key_field_unparsed`. Приём тот же, что у `mismatch_class` в
|
||||
витрине сверки: пересекающиеся классы плюс объявленный приоритет.
|
||||
|
||||
Присутствие полей целиком держит сверка набора ключей — одно сравнение
|
||||
отсортированного `JSONExtractKeys` с контрактным списком. Обязательны все сорок
|
||||
`arraySort(JSONExtractKeys(raw))` с контрактным списком, завёрнутым в тот же
|
||||
`arraySort`. Обёртка с обеих сторон стоит ноль и снимает ошибку, которая иначе
|
||||
увела бы в брак вообще всё: сорок семь CamelCase-имён, выписанных руками ровно в
|
||||
байтовом порядке. Обязательны все сорок
|
||||
семь полей: генератор шлёт их все в каждом событии, а «пусто» по контракту —
|
||||
пустое значение, а не отсутствие ключа. Этим же закрыт критерий #43 про опечатку
|
||||
в имени.
|
||||
@@ -40,6 +53,16 @@ JSON.
|
||||
'stream'` не используется тоже: при чтении байтами на входе нечему ломаться, и
|
||||
ошибке разбора взяться неоткуда.
|
||||
|
||||
Отсюда ограничение на форму выражений разбора: они собираются только из функций,
|
||||
которые не бросают исключений. `JSONExtract` и родственные возвращают значение по
|
||||
умолчанию или NULL, но не падают, — и правило репозитория «грязные записи не
|
||||
валят пайплайн» держится теперь именно на этом. Исключение в матвью не ошибка
|
||||
формата, его не перехватит никакой режим Kafka-движка: вставка упадёт, офсеты не
|
||||
закоммитятся, блок пойдёт читаться снова. Ломается это громко и чинится без
|
||||
потерь — поправил матвью, потребление продолжилось с некоммиченного офсета, — но
|
||||
пока не починено, топик стоит. Поэтому приведение `Nullable` к необнуляемому типу
|
||||
и любая арифметика в этих выражениях живут за предикатом, который NULL уже отсёк.
|
||||
|
||||
Этим решение снимает ограничение, записанное в постановке #37: «ошибки разбора
|
||||
должны рождаться на шаге Kafka-движка». Оно ставилось как условие достижимости
|
||||
критерия #43 про громкую ошибку на опечатку в имени поля — критерий достижим и
|
||||
@@ -116,26 +139,42 @@ contract-тест из #43 сюда не дотягивается — он ср
|
||||
сообщений, которые не разобрались, и оставляет их пустыми для разобранных.
|
||||
Отсюда весь довод о том, что типизированный чтец не может наполнить слой сырья.
|
||||
|
||||
Составные типы `Nullable` не оборачивает: `Nullable(Array)`, `Nullable(Map)` и
|
||||
`Nullable(Tuple)` не поддерживаются, а `Nullable` внутри них — да. Отсюда
|
||||
слепота `Nullable`-разбора на двенадцати колонках и необходимость сверки ключей.
|
||||
`Nullable` внутри кортежа разрешён, поэтому запасной вариант со свёрткой в
|
||||
именованный кортеж жив.
|
||||
Составные типы `Nullable` не оборачивает: `Nullable(Array)` и `Nullable(Map)` не
|
||||
поддерживаются, а `Nullable` внутри них — да. Отсюда слепота `Nullable`-разбора
|
||||
на двенадцати колонках и необходимость сверки ключей. Оговорка про кортеж:
|
||||
`Nullable(Tuple)` в ClickHouse всё-таки есть, но за настройкой
|
||||
`enable_nullable_tuple_type` и в статусе беты; на довод это не влияет — кортежей
|
||||
в контракте нет. `Nullable` внутри кортежа разрешён без всяких флагов, поэтому
|
||||
запасной вариант со свёрткой в именованный кортеж жив.
|
||||
|
||||
Оттуда же: у Kafka-движка есть третий режим обработки ошибок —
|
||||
`dead_letter_queue` с записью в системную таблицу. Он отвергнут независимо от
|
||||
остального: спека требует свои `*_errors`, системная таблица их не заменяет.
|
||||
|
||||
Форматы `RawBLOB` и `LineAsString` в ClickHouse есть, и Kafka-движок
|
||||
поддерживает все форматы.
|
||||
поддерживает все форматы. `RawBLOB` читает вход в одно значение и рассчитан на
|
||||
таблицу с единственным полем `String` — отсюда ограничение на форму чтеца.
|
||||
|
||||
Матвью с источником-`Distributed` срабатывает на вставку именно в эту
|
||||
распределённую таблицу — блок она видит до разрезания по шардам. Проверено
|
||||
владельцем на рабочих проектах; на стенде подтверждается заодно с приёмкой #37.
|
||||
|
||||
На живом стенде проверяется при исполнении #37, и то же внесено в раздел 11
|
||||
спеки:
|
||||
На живом стенде проверяется при исполнении #37. Первые два пункта внесены в
|
||||
раздел 11 спеки как несущие; остальные — однострочные `SELECT`, их довольно
|
||||
прогнать заодно:
|
||||
|
||||
- `RawBLOB` в Kafka-движке даёт ровно одну строку на сообщение;
|
||||
- `RawBLOB` в Kafka-движке даёт ровно одну строку на сообщение. Проверять это
|
||||
нужно первым и до написания DDL: формулировка «читает вход в одно значение»
|
||||
про файл понятна, а про пачку сообщений из топика — нет, и если сообщения
|
||||
склеятся, переделывать придётся решение целиком, а не DDL. Опыт стоит трёх
|
||||
сообщений и одного `count()`. Запасной вариант — `LineAsString`: он режет по
|
||||
переводу строки, а события у нас однострочные; цена запасного — сообщение с
|
||||
переводом строки внутри даст две строки вместо одной;
|
||||
- форма именованного кортежа в `JSONExtract` с `Nullable`-членами — нужна для
|
||||
свёртки сорока семи вызовов в один, если разбор окажется дорогим.
|
||||
свёртки сорока семи вызовов в один, если разбор окажется дорогим;
|
||||
- `isValidJSON('123')` возвращает единицу, а `JSONExtractKeys` от скаляра —
|
||||
пустой массив. На обоих стоят классы брака и их приоритет, а документация
|
||||
поведение на не-объекте не описывает: два `SELECT` закрывают вопрос;
|
||||
- `JSONAsString` действительно падает на некорректном JSON, а не пропускает
|
||||
строку. На этом стоит отказ от него в пользу `RawBLOB`; документация про
|
||||
ошибочный ввод молчит.
|
||||
|
||||
Reference in New Issue
Block a user