From c361c36e3d29dc55368f39cfb102b2887c91813f Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Wed, 26 Aug 2026 06:38:46 -0400 Subject: [PATCH] =?UTF-8?q?docs(adr):=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0=20=D0=B2=20ODS=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D0=BC=D0=BE=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=B0=20=D0=B4=D0=B0=D1=82=D1=83=20=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=BF=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - лотерея ярлыков доставки при ничьей по updated_at делала слой недетерминированным и держала окно K двумя копиями без сверки (#163). - Что: - добавлен ADR 0014: версия ods.order — snapshot_date, с ответом доводу ADR 0010, границами допущения и веером отклонённых вариантов. - в ADR 0010 помечен пересмотр колонки версии. - Проверка: - реализация и замеры — тикет #165; решение кодом пока не применено. Co-Authored-By: Claude Fable 5 --- docs/adr/0010-order-versions-in-ods.md | 4 +- docs/adr/0014-order-version-snapshot-date.md | 96 ++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0014-order-version-snapshot-date.md diff --git a/docs/adr/0010-order-versions-in-ods.md b/docs/adr/0010-order-versions-in-ods.md index 8d5fca2..df7fa25 100644 --- a/docs/adr/0010-order-versions-in-ods.md +++ b/docs/adr/0010-order-versions-in-ods.md @@ -1,7 +1,9 @@ # ADR 0010. Заказы в ODS: версии сущности вместо подмены слепка Дата: 16 августа 2026 года. Статус: принято. Частично заменяет -[ADR 0008](0008-order-ingestion.md). +[ADR 0008](0008-order-ingestion.md). Колонка версии пересмотрена +[ADR 0014](0014-order-version-snapshot-date.md): вместо `updated_at` — +`snapshot_date`. ## Решение diff --git a/docs/adr/0014-order-version-snapshot-date.md b/docs/adr/0014-order-version-snapshot-date.md new file mode 100644 index 0000000..9418e7f --- /dev/null +++ b/docs/adr/0014-order-version-snapshot-date.md @@ -0,0 +1,96 @@ +# ADR 0014. Версия заказа в ODS: дата слепка + +Дата: 26 августа 2026 года. Статус: принято. Пересматривает колонку версии +из [ADR 0010](0010-order-versions-in-ods.md); остальные решения того ADR в +силе. Развилка разобрана в #163, реализация — #165. + +## Решение + +`ods.order` хранит версии заказа в `ReplacingMergeTree(snapshot_date)`: при +схлопывании выживает строка позднейшего слепка, который нёс заказ. Формула +решения: у полного слепка версия — сам слепок; истина — то, что источник +сказал последним. + +Ключ сущности, партиция и шардирование не меняются. `updated_at` остаётся +деловым полем, и деловой ответ слоя от смены версии не зависит: на +канонических слепках выбор по дате слепка побайтово совпадает с выбором по +деловой метке, потому что метка монотонна по слепкам, а слепок дня — чистая +функция зерна и дня (тесты генератора +`test_the_state_is_read_at_the_boundary_of_the_day` и +`test_two_runs_give_the_same_snapshot`). + +Решение покупает две вещи. + +- **Детерминизм слоя.** Разные слепки несут разные даты — ничьей между + содержательно разными строками не бывает. При версии `updated_at` доставки + неизменившегося заказа равны по версии, и выжившая строка — жребий: у 86 + заказов стартового мира выживала строка серединного слепка, притом что у + всех 3034 пар строк с равным `updated_at` деловые поля совпадали побайтово + (замеры #137). Документация ClickHouse обещает при ничьей победу последней + вставленной строки — замер это опроверг, полагаться на неё было бы ошибкой. +- **Правдивый ярлык.** `snapshot_date` выжившей строки — последний слепок, + который нёс заказ. Объём пересборки DDS читается прямо из данных, причём + без `FINAL`: слияние сохраняет максимум даты по построению. Константа + окна K уходит со стороны стенда вместе с риском дрейфа её двух копий + (ревью #151 ставило этому риску MAJOR). + +## Ответ доводу ADR 0010 + +ADR 0010 отверг семейство «публикацию решает дата наблюдения»: у потребителя +нет признака полноты слепка. Довод целил в подмену партиции — ей полнота +нужна, потому что неполная партиция стёрла бы уже принятые версии. Колонке +версии полнота не нужна: она сравнивает свежесть строк одного заказа, и +недоехавшая строка просто оставляет в силе предыдущую — ровно как при версии +по деловой метке. + +## Границы допущения + +- **Дисциплина слепков.** Слепок с поздней датой не несёт состояния старше. + Источник, собирающий слепок из отставшей реплики, это ломает; модельный + мир — нет: см. тесты выше. +- **Одна дата — один слепок.** Ограничение вшито в движок: вторая + внутридневная выгрузка дала бы ничью уже на деловых полях. Это условие + контракта источника, его место — в границе строгого приёма + ([приём заказов](../architecture/orders/ingestion.md)). +- **Окно наследует потерю строк.** Частичный отказ вставки после коммита + офсетов может унести старший день последнего слепка — выведенное окно + молча сузится. Лекарство прежнее, сверка слоя с источником в контуре DQ + ([конвейер](../architecture/etl.md), «Чего конвейер о себе не знает»). +- **Тихая правка лечится только внутри окна.** Строку, изменённую без + сдвига `updated_at`, перекроет следующий слепок; замёрзший заказ источник + больше не показывает, там лечит лишь сверка полей. +- **Повторная доставка слепка.** Байты совпадают, жребий остаётся только на + `_load_id` и `_load_ts`; ярлык даты детерминирован. +- **Правка живого мира не подхватится.** DDL исполняется при рождении мира, + `ALTER` колонку версии не меняет: решение вступает в силу пересозданием + хранилища (`make rebuild-storage`). + +## Отклонённые варианты + +- **Оставить как есть, сверку копий K отдать DQ.** Лотерея ярлыков остаётся + задокументированным грехом, окно — константой с риском дрейфа. +- **Убрать координаты доставки из ODS.** Наказывает поле за вопрос, на + который оно не бралось отвечать: расследованию (связка годных строк, + ошибок и сырья) координаты служили честно. +- **Дедупликация на вставке.** Детерминизм «обычно», не по построению: + анти-джойн с собственной целью не атомарен, лаг реплик протаскивает дубли. + Приём теряет урок «разобрал и вставил», а ярлык со смыслом «первый + носитель» хуже для обеих ролей поля: окну нужен последний носитель, и + сырьё живо скорее у него. +- **Склейка `(updated_at, snapshot_date)` в одну колонку версии.** Движок + берёт одну колонку, склейка требует битовой упаковки. Робастнее к + недисциплинированному источнику, но платит читаемостью DDL за защиту от + сценариев, которых модельный мир не производит. +- **Склейка `(snapshot_date, updated_at)`.** Путь отступления: если у + источника появятся переиздания слепка одной даты, версия дорастает до + этой формы. От правок без сдвига метки внутри одной даты не спасает и она. + +## Связанные решения + +- [ADR 0010](0010-order-versions-in-ods.md) — версии заказа в ODS; здесь + пересмотрена только колонка версии. +- [Приём заказов](../architecture/orders/ingestion.md) — граница строгого + приёма и четыре координаты строки. +- [Модель заказа в DDS](../architecture/dds/order.md) — читатель окна + пересборки. +- Развилка, веер и два холодных ревью — #163.