Consumer: DevStatsBuildingUpsertedConsumer
Ingest-консюмер реплики-измерения «Здание» (литер/корпус) в dev-stats: принимает snapshot здания из
chessboard и upsert-ит строку в ClickHouse-измерение dim_building
(ReplacingMergeTree, pin_dev_stats). Опциональный уровень Building иерархии обогащения фактов
(ADR-0057). Дом payload — event-contract: building-upserted;
дом колонок — dimensions/building.md.
Назначение
Consumer DevStatsBuildingUpsertedConsumer слушает событие building-upserted (топик
pin.chessboard.building-upserted.v1) и пишет срез здания (Name/Status/ConstructionStatus/FloorsTotal
ProjectId/DeveloperId-родители + версия) вdim_building. Измерение даёт подпись графика (Name— «Литер 3») и денормализованныйDeveloperId(изTenantId), чтобы восхождение по иерархии и проверка scope шли без обращения кdim_project. Owned-состояния в PostgreSQL нет (ClickHouse-only); сроки сдачи (Delivery*),SectionsCount,ReadinessPercent, счётчики объектов недвижимости не реплицируются.
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | dev-stats (проект DevStats.Background) |
| Consumer | DevStatsBuildingUpsertedConsumer |
| Обрабатываемое событие | building-upserted |
| Topic | pin.chessboard.building-upserted.v1 |
| ContractVersion | 1; при несовпадении — warn-and-proceed |
| Consumer group | dev-stats-dim-building |
| Producer | chessboard (Developers API create/update/set-status здания, синк Talent, job project-aggregates-recalc при мутации snapshot-поля) |
| Эффект (событие → что меняется) | upsert строки в pin_dev_stats.dim_building (ReplacingMergeTree) |
Контекст события
- Из метаданных события доступны идентификатор источника (
AggregateId = Idздания), тип агрегата (Building), время события (CreatedAt→ колонкаUpsertedAt, версияReplacingMergeTree(UpsertedAt)), версия snapshot-а (Version). - Сквозной трейс восстанавливается из метаданных события.
- Идемпотентность — декларативная, на стороне ClickHouse: dedup-ключ =
BuildingId(единственный компонентORDER BY), версионирование —ReplacingMergeTree(UpsertedAt). Порядок per-building гарантирован ключом сообщения (key = Id); отдельная таблица идемпотентности не используется (PG-транзакции нет).
Входные данные / payload события
Ссылка на контракт: event-contract: building-upserted. Дом и типы колонок — dimensions/building.md, раздел «Columns»; ниже — поля, которые консюмер реально читает, и маппинг.
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
Id | Guid | Да | → BuildingId (UUID, ключ ORDER BY); Guid.Empty → poison (DLQ) |
ProjectId | Guid | Да | → ProjectId (родитель); Guid.Empty → poison |
TenantId | Guid | Да | → DeveloperId (денормализация вверх по иерархии, ADR-0052); Guid.Empty → poison |
Name | string | Да | → Name («Литер 3», подпись графика) |
Status | string (enum BuildingStatus) | Да | → Status (LowCardinality): Draft/Published/Hidden; unknown → сохранить как есть + warning + метрика (на чтении — как Hidden) |
ConstructionStatus | string (enum) | Да | → ConstructionStatus; unknown → сохранить как есть + warning (fallback чтения UnderConstruction) |
FloorsTotal | short | Да | → FloorsTotal (Int16, подпись корпуса) |
Version | long | Да | → SourceVersion (guard/диагностика) |
SectionsCount,DeliveryYear,DeliveryQuarter,ReadinessPercent,PropertyObjects*Count | — | — | не реплицируются |
Колонки, заполняемые консюмером: UpsertedAt = время события.
Алгоритм
Консюмер батчевый: шаги 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_building(DeveloperId = TenantId,UpsertedAt= время события,SourceVersion = Version); переходов состояний нет. Добавить в буфер; внутрибатчевая свёртка поBuildingId(maxUpsertedAt). - Пакетно вставить (upsert) буфер строк в
pin_dev_stats.dim_building. Upsert обеспечиваетReplacingMergeTree(UpsertedAt)(merge → maxUpsertedAtperBuildingId); точечное обновление/удаление не применяются (Status = Hidden— обычная строка snapshot-а). Смещение (offset) фиксируется строго после подтверждённой вставки; при ошибке — retry всего батча.
Диаграмма
Идемпотентность и replay
- Business key:
BuildingId; версия-precedence —UpsertedAt(ReplacingMergeTree). Дубль перезаписывается идемпотентно; до merge чтение дубль-устойчиво —argMax(Name, UpsertedAt) … GROUP BY BuildingId(FINALзапрещён). - Version guard (dimensions/building.md) — декларативный: per-key-ordering
ReplacingMergeTree(UpsertedAt)+ внутрибатчевая свёртка; version-lookup нет (ADR-0056).
- Replay/backfill: да — повтор топика
building-upsertedгруппойdev-stats-dim-buildingбезопасен; первичное наполнение — 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), unknown-enum (Status/ConstructionStatus). - Metrics:
dev_stats_dim_building_upserted_total,dev_stats_dim_ingest_lag_seconds,dev_stats_dim_ingest_batch_size,chessboard_sync_unknown_enum_total, retry/DLQ. - Traces: consume span + insert span; корреляция из метаданных события.
- PII/masking: PII отсутствует (здание — объект). Tenant:
DeveloperId=TenantId(ADR-0052); scope-фильтрация — на чтении в API.
Кросс-сервисные контракты
| Направление | Контракт |
|---|---|
Ожидает от chessboard | Событие building-upserted (pin.chessboard.building-upserted.v1, ContractVersion = 1, at-least-once). Гарантии: ключ сообщения = Id (per-building ordering), монотонная Version, денормализованный TenantId = DeveloperId в snapshot-е. ProjectId — родитель здания. |
| Предоставляет | Строку dim_building (ReplacingMergeTree) с Name/Status/ConstructionStatus/FloorsTotal + ProjectId/DeveloperId. Потребители реплики: обогащение фактов просмотров уровнем Building (опциональный уровень иерархии) и подписи графиков ЛК («Литер 3»). |
| Окно eventual-consistency | ≤ 5 мин. Пока реплика отстаёт, факт просмотра берёт последнюю известную версию dim_building при восхождении по иерархии; подпись здания может отставать. |
Предусловия и постусловия
- Предусловия: миграции DDL измерения применены; событие валидно (
Id/ProjectId/TenantId != Guid.Empty). - Постусловия: в
dim_buildingдляBuildingIdактуальна строка с максимальнымUpsertedAt; offset группыdev-stats-dim-buildingпродвинут строго после подтверждённой вставки; операционные данныеchessboardне изменены.
Acceptance Criteria
- Happy path (создание): Дано — миграции применены; Когда — приходит
building-upserted(B1,ProjectId = P1,Name = "Литер 3"); Тогда —argMax(Name, UpsertedAt) … GROUP BY BuildingIdдляB1= "Литер 3",DeveloperId = TenantId. - Обновление (новая версия): Дано — строка
B1есть; Когда — событие с большимUpsertedAtи новымStatus; Тогда — чтение отдаёт новый статус (старая версия схлопывается при merge). - Идемпотентность (повтор доставки): Дано — событие
B1применено; Когда — повторная доставка; Тогда — логических дублей нет, offset продвинут. - Poison: Дано —
Id/ProjectId/TenantId == Guid.Emptyили битый payload; Тогда — DLQ (poison) + error-лог, партиция продолжается. - Unknown-enum: Дано —
Status/ConstructionStatusвне набора; Тогда — значение сохраняется как есть + warning + метрика; на чтении fallback (Hidden/UnderConstruction). - Отказ хранилища: Дано — ClickHouse недоступен; Тогда — offset не коммитится, retry с backoff, догон без дублей.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События dev-stats, Event Contract: pin.chessboard.building-upserted (BuildingUpsertedKafkaEvent)
- Консюмеры (5): Consumer: DevStatsDeveloperUpsertedConsumer, Consumer: DevStatsLayoutGroupUpsertedConsumer, Consumer: DevStatsLayoutUpsertedConsumer, Consumer: DevStatsProjectUpsertedConsumer, Consumer: DevStatsPropertyObjectUpsertedConsumer
Связанные документы
- Дом колонок и dedup: dimensions/building.md; контракт события: chessboard/events/building-upserted.md.
- Соседние ingest-консюмеры: developer-upserted.md, project-upserted.md, layout-group-upserted.md, layout-upserted.md, property-object-upserted.md.
- Обзор: ../README.md; backfill: ../background/dimension-backfill.md.
- ADR-0052, ADR-0056, ADR-0057.