Consumer: DevStatsLayoutGroupUpsertedConsumer
Ingest-консюмер реплики-измерения «Группа планировок» в dev-stats: принимает snapshot группы из
chessboard и upsert-ит строку в ClickHouse-измерение
dim_layout_group (ReplacingMergeTree, pin_dev_stats). Уровень
иерархии между проектом и планировкой (ADR-0057). Дом payload —
event-contract: layout-group-upserted; дом колонок —
dimensions/layout-group.md.
Назначение
Consumer DevStatsLayoutGroupUpsertedConsumer слушает событие layout-group-upserted (топик
pin.chessboard.layout-group-upserted.v1) и пишет срез группы (Name/SortOrder/IsArchived
ProjectId/DeveloperId-родители + версия) вdim_layout_group. Измерение даёт подпись графика (Name— «Студии», «Люкс с видом на море»), порядок отображения (SortOrder) и денормализованныйDeveloperIdдля восхождения по иерархии/фильтрации без JOIN-ов. У группы нет статусного цикла — вместоStatusреплицируется флагIsArchived. Owned-состояния в PostgreSQL нет; счётчики (LayoutsCount) и площади (AreaMin/AreaMax) не реплицируются.
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | dev-stats (проект DevStats.Background) |
| Consumer | DevStatsLayoutGroupUpsertedConsumer |
| Обрабатываемое событие | layout-group-upserted |
| Topic | pin.chessboard.layout-group-upserted.v1 |
| ContractVersion | 1; при несовпадении — warn-and-proceed |
| Consumer group | dev-stats-dim-layout-group |
| Producer | chessboard (Developers API create/update/archive группы, синк Talent, job project-aggregates-recalc) |
| Эффект (событие → что меняется) | upsert строки в pin_dev_stats.dim_layout_group (ReplacingMergeTree) |
Контекст события
- Из метаданных события доступны идентификатор источника (
AggregateId = Idгруппы), тип агрегата (LayoutGroup), время события (CreatedAt→UpsertedAt, версияReplacingMergeTree(UpsertedAt)), версия snapshot-а (Version). - Сквозной трейс восстанавливается из метаданных события.
- Идемпотентность — декларативная, на стороне ClickHouse: dedup-ключ =
LayoutGroupId(единственный компонентORDER BY), версионирование —ReplacingMergeTree(UpsertedAt). Порядок per-group гарантирован ключом сообщения (key = Id); отдельная таблица идемпотентности не используется (PG-транзакции нет).
Входные данные / payload события
Ссылка на контракт: event-contract: layout-group-upserted. Дом и типы колонок — dimensions/layout-group.md, раздел «Columns»; ниже — поля, которые консюмер реально читает, и маппинг.
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
Id | Guid | Да | → LayoutGroupId (UUID, ключ ORDER BY); Guid.Empty → poison (DLQ) |
ProjectId | Guid | Да | → ProjectId (родитель); Guid.Empty → poison |
TenantId | Guid | Да | → DeveloperId (денормализация вверх, ADR-0052); Guid.Empty → poison |
Name | string | Да | → Name (подпись графика) |
SortOrder | int | Да | → SortOrder (Int32, порядок отображения групп) |
IsArchived | bool | Да | → IsArchived (UInt8): true → 1, false → 0 (архив — пометка, строка не удаляется) |
Version | long | Да | → SourceVersion (guard/диагностика) |
LayoutsCount,AreaMin,AreaMax | — | — | не реплицируются |
Колонки, заполняемые консюмером: UpsertedAt = время события. Enum-полей в payload нет — обработка
unknown-enum неприменима (состояние группы — bool IsArchived).
Алгоритм
Консюмер батчевый: шаги 1–3 — на сообщение, шаг 4 — на батч (порог ≤ 1000 строк или 5 секунд; freshness SLA ≤ 5 мин).
- Проверить контекст события: сверить
ContractVersion(=1; при несовпадении — warning-лог и продолжить). Дубль на входе не отсеивается (dedup декларативен). - Проверить payload: ошибка десериализации /
Id/ProjectId/TenantId == Guid.Empty→ DLQ (poison) + error-лог. Проверки существования родителей нет:DeveloperIdберётся изTenantId, резолв иерархии факта идёт изdim_property_object. - Маппинг поле-в-поле: построить строку
dim_layout_group(DeveloperId = TenantId,IsArchived = payload.IsArchived ? 1 : 0,UpsertedAt= время события,SourceVersion = Version). Добавить в буфер; внутрибатчевая свёртка поLayoutGroupId(maxUpsertedAt). - Пакетно вставить (upsert) буфер строк в
pin_dev_stats.dim_layout_group. Upsert обеспечиваетReplacingMergeTree(UpsertedAt)(merge → maxUpsertedAtperLayoutGroupId); точечное обновление/удаление не применяются (архив — upsert сIsArchived = 1). Смещение (offset) фиксируется строго после подтверждённой вставки; при ошибке — retry всего батча.
Диаграмма
Идемпотентность и replay
- Business key:
LayoutGroupId; версия-precedence —UpsertedAt(ReplacingMergeTree). Дубль перезаписывается идемпотентно; до merge чтение дубль-устойчиво —argMax(Name, UpsertedAt) … GROUP BY LayoutGroupId(FINALзапрещён). - Version guard (dimensions/layout-group.md) — декларативный:
per-key-ordering +
ReplacingMergeTree(UpsertedAt)+ внутрибатчевая свёртка; version-lookup нет (ADR-0056). - Replay/backfill: да — повтор топика
layout-group-upsertedгруппойdev-stats-dim-layout-groupбезопасен; первичное наполнение — background/dimension-backfill.md.
Retry и DLQ
| Сбой | Retry | DLQ | Ручное действие |
|---|---|---|---|
| ClickHouse недоступен / timeout вставки | Да: retry батча, backoff (1с → 60с), offset не commit-ится | Нет (инфраструктура) | Алерт ingest-lag > 15 мин; восстановить CH |
| Ошибка десериализации payload | Нет | Да (poison) | Разбор из DLQ; issue producer-у chessboard |
Id/ProjectId/TenantId == Guid.Empty (шаг 2) | Нет | Да (poison) + error-лог | Проверить контракт; replay из DLQ после фикса |
Версия события ≠ ContractVersion | Не сбой: warning-лог | Нет | Сверить миграцию контракта |
| Сбой между вставкой и commit offset | Повторная доставка (at-least-once) | Нет | Не требуется — dedup движком |
Наблюдаемость и безопасность
- Logs: consumed, skipped-poison (
Id), flushed (строк, длительность), failed (CH). - Metrics:
dev_stats_dim_layout_group_upserted_total,dev_stats_dim_ingest_lag_seconds,dev_stats_dim_ingest_batch_size, retry/DLQ. - Traces: consume span + insert span; корреляция из метаданных события.
- PII/masking: PII отсутствует (группа планировок — справочный объект). Tenant:
DeveloperId=TenantId(ADR-0052); scope-фильтрация — на чтении в API.
Кросс-сервисные контракты
| Направление | Контракт |
|---|---|
Ожидает от chessboard | Событие layout-group-upserted (pin.chessboard.layout-group-upserted.v1, ContractVersion = 1, at-least-once). Гарантии: ключ сообщения = Id (per-group ordering), монотонная Version, TenantId = DeveloperId и ProjectId-родитель в snapshot-е. Статусного цикла нет — состояние выражено bool IsArchived. |
| Предоставляет | Строку dim_layout_group (ReplacingMergeTree) с Name/SortOrder/IsArchived + ProjectId/DeveloperId. Потребители реплики: обогащение фактов уровнем LayoutGroup и подписи/порядок групп в ЛК («Студии», «Люкс с видом на море»). |
| Окно eventual-consistency | ≤ 5 мин. Пока реплика отстаёт, факт берёт последнюю известную версию dim_layout_group; подпись/порядок группы может отставать. |
Предусловия и постусловия
- Предусловия: миграции DDL измерения применены; событие валидно (
Id/ProjectId/TenantId != Guid.Empty). - Постусловия: в
dim_layout_groupдляLayoutGroupIdактуальна строка с максимальнымUpsertedAt(архив — upsert сIsArchived = 1, строка не удаляется); offset группыdev-stats-dim-layout-groupпродвинут строго после подтверждённой вставки; операционные данныеchessboardне изменены.
Acceptance Criteria
- Happy path (создание): Дано — миграции применены; Когда — приходит
layout-group-upserted(G1,ProjectId = P1,Name = "Студии",IsArchived = false); Тогда —argMax(Name, UpsertedAt) … GROUP BY LayoutGroupIdдляG1= "Студии",IsArchived = 0,DeveloperId = TenantId. - Архивация: Дано — строка
G1есть; Когда — событие сIsArchived = trueи большимUpsertedAt; Тогда — чтение отдаётIsArchived = 1(строка сохраняется, не удаляется). - Идемпотентность (повтор доставки): Дано — событие
G1применено; Когда — повторная доставка; Тогда — логических дублей нет, offset продвинут. - Poison: Дано —
Id/ProjectId/TenantId == Guid.Emptyили битый payload; Тогда — DLQ (poison) + error-лог, партиция продолжается. - Отказ хранилища: Дано — ClickHouse недоступен; Тогда — offset не коммитится, retry с backoff, догон без дублей.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События dev-stats, Event Contract: pin.chessboard.layout-group-upserted (LayoutGroupUpsertedKafkaEvent)
- Консюмеры (5): Consumer: DevStatsBuildingUpsertedConsumer, Consumer: DevStatsDeveloperUpsertedConsumer, Consumer: DevStatsLayoutUpsertedConsumer, Consumer: DevStatsProjectUpsertedConsumer, Consumer: DevStatsPropertyObjectUpsertedConsumer
Связанные документы
- Дом колонок и dedup: dimensions/layout-group.md; контракт события: chessboard/events/layout-group-upserted.md.
- Соседние ingest-консюмеры: developer-upserted.md, project-upserted.md, building-upserted.md, layout-upserted.md, property-object-upserted.md.
- Обзор: ../README.md; backfill: ../background/dimension-backfill.md.
- ADR-0052, ADR-0056, ADR-0057.