feat(dds): добавлена модель сессии и нарезка визитов окнами

Зачем:
- Дать слою расчётную сущность, которой в потоке нет: стенд режет визиты сам,
  а VisitID генератора остаётся эталоном самопроверки.
- Отделить прожитый день от живого — иначе визит, начатый в 23:50, замёрз бы
  обрезанным навсегда: пересобирать замёрзший день слой не станет.

Что:
- Добавлены dds.session_rep/_dist, двойник-донор dds.session_stage_rep/_dist и
  публичный договор dds.session_v.
- Нарезка стоит на двух окнах: первое ставит шов там, где пауза длиннее
  тридцати минут, второе считает нарастающий итог швов — номер визита. Границу
  суток держит разбиение окна, отдельной проверки ей не нужно.
- Порядок в окнах полный, по метке времени и event_id: сортировок две, и при
  равных метках они разошлись бы, разорвав сессию надвое.
- Объём прогона считается от первой дыры в слое по последний прожитый день;
  прожитый день выводится из позиции мира и первого дня событий. Собирать
  нечего — группа задач пропускается, а не зеленеет впустую.
- В dds_transform появилась группа session рядом с заказом; зависимости по
  данным между ними нет.
- Потолок max_active_tasks: размноженная замена без него выносит планировщика
  за отведённые ему 640 МиБ.
- parallel_distributed_insert_select у сессий не назначается вслух — ключ
  шардирования у источника и цели один по контракту ко-локации.
- Записано, что позиция отвечает за «прожит», а не за «доехал»: сторож хвоста
  потока приезжает отдельным тикетом.

Проверка:
- make lint
- make smoke (21 проверка), make check-clickhouse (10), make check-services
- Стартовый мир, пересобранный с нуля: 79367 сессий за восемь дней;
  расхождение с uniqExact(VisitID) по паре «кука × день» — ноль; все 401185
  событий ровно в одной сессии; session_id уникальны.
- Ни одной паузы длиннее тридцати минут внутри сессии и ни одной пары соседних
  визитов куки, разделённой меньшим промежутком.
- Повтор после DROP PARTITION ALL: те же session_id и то же содержимое строк.
- Снесённая руками партиция 2026-06-04: следующий прогон взял хвост от неё,
  дни до дыры не тронуты.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ddadminandClaude Opus 5 committed 2026-08-26 07:56:44 -04:00
1 parent c361c36e3d
commit 176cf5520d
9 files changed
+495 -60

No files matched your search

+22 -8
View File
@@ -40,8 +40,8 @@
## Объём прогона
Параметров у конвейера нет: объём он выводит сам из `world_position` и
состояния слоёв. Сессии догоняют хвост — от последнего собранного дня до
последнего прожитого; заказы пересобирают дышащее окно K и тот же хвост; карта
состояния слоёв. Сессии догоняют хвост — от первого несобранного дня по
последний прожитый; заказы пересобирают дышащее окно K и тот же хвост; карта
идентичностей пересчитывается целиком; правила витрин — у
[слоя DM](dm.md).
Поэтому один и тот же запуск одинаково верен из триггера, с кнопки менти и
@@ -56,6 +56,16 @@
неудачного приёма она обгоняет хранилище. Слой обязан считать объём по тому,
что у него есть, а не по тому, что мир о себе заявляет.
У сессий всё наоборот: им отвечает только позиция, потому что вопрос у них
другой. Им нужен не день, о котором рассказал источник, а день, который мир
**прожил**: события живой день от прожитого не отличают — и посреди дня D, и
сразу после его конца позднейший день событий равен D. Соберись сессии посреди дня, визит, начатый в
23:50, остался бы обрезанным навсегда: замёрзший день слой не пересобирает.
Отсюда правило — последний прожитый день равен первому дню событий плюс позиция
минус один ([`sql/dds/session_scope.sql`](../../sql/dds/session_scope.sql)).
Оговорка про «доехал» остаётся в силе и здесь: за хвост потока отвечает не
позиция, а «Готовность источника».
Ярлыки доставки — `snapshot_date`, `_load_id`, `_load_ts` — не отвечают тоже, и
это свойство ODS, а не недосмотр: слой схлопывает доставки, сохраняя деловые
версии, поэтому у заказа, который не менялся, ярлык выжившей строки достаётся
@@ -67,14 +77,18 @@
## Готовность источника
Первый шаг `dds_transform` — дождаться, пока хвост топика `hits` дожёван:
отставание его потребителей нулевое (`system.kafka_consumers`). Зелёный
приём заказов о событиях не говорит ничего — топики разные и потребители
разные. В живом режиме зазор ничтожен, но в пакетном генератор выстреливает
дни в топик быстрее, чем чтец их прожёвывает, и сессии собрались бы по
недожёванному дню. DQ-сверка такой день подсветит, но починить его до
Первым шагом `dds_transform` обязан дождаться, пока хвост топика `hits`
дожёван: отставание его потребителей нулевое (`system.kafka_consumers`).
Зелёный приём заказов о событиях не говорит ничего — топики разные и
потребители разные. В живом режиме зазор ничтожен, но в пакетном генератор
выстреливает дни в топик быстрее, чем чтец их прожёвывает, и сессии собираются
по недожёванному дню. DQ-сверка такой день подсветит, но починить его до
этапа 5 нечем: профилактика дешевле ремонта, которого ещё нет.
Шага этого в даге ещё нет — он приезжает отдельным тикетом (#156), и до тех
пор дыра открыта: сессии считают день прожитым по позиции мира, а хвост потока
не стережёт никто. На приёмке отставание смотрят руками.
## Идемпотентность
Партиция не заменяется простой вставкой. День собирается в таблицу-двойник —