Consumer: DevStatsDeveloperUpsertedConsumer
Один из шести ingest-консюмеров реплик-измерений dev-stats: принимает snapshot застройщика из контекста
chessboard и upsert-ит строку в ClickHouse-измерение dim_developer
(ReplacingMergeTree, pin_dev_stats). Верхнее измерение иерархии
Developer → Project → Building → LayoutGroup → Layout → PropertyObject, из которой обогащаются факты просмотров
(ADR-0057). Дом payload — event-contract: developer-upserted;
дом колонок — dimensions/developer.md.
Назначение
Consumer DevStatsDeveloperUpsertedConsumer слушает событие developer-upserted (топик
pin.chessboard.developer-upserted.v1) и пишет минимальный срез профиля застройщика (Name/LegalName/
Status/LogoFileId + версия) в dim_developer. Назначение измерения — JOIN-free-подпись и проверка
scope: факты уже несут денормализованный DeveloperId, а измерение резолвит человекочитаемое имя/лого для
подписей графиков ЛК и подтверждает существование застройщика для scope-контроля (DeveloperId = актор).
Owned-состояния в PostgreSQL нет — контекст ClickHouse-only (ADR-0057); показатели карточки (Highlights),
FoundedYear, контакты не реплицируются (в dim_developer не нужны).
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | dev-stats (проект DevStats.Background) |
| Consumer | DevStatsDeveloperUpsertedConsumer |
| Обрабатываемое событие | developer-upserted |
| Topic | pin.chessboard.developer-upserted.v1 |
| ContractVersion | 1; при несовпадении — warn-and-proceed |
| Consumer group | dev-stats-dim-developer |
| Producer | chessboard (Developers API профиля, consumer компаний identity при Type = Developer, синк Talent) |
| Эффект (событие → что меняется) | upsert строки в pin_dev_stats.dim_developer (ReplacingMergeTree) |
Контекст события
- Из метаданных события доступны идентификатор источника (
AggregateId = Idзастройщика), тип агрегата (Developer), время события (CreatedAt→ колонкаUpsertedAt, версия движкаReplacingMergeTree(UpsertedAt)), версия snapshot-а (Version). - Сквозной трейс восстанавливается из метаданных события.
- Идемпотентность — декларативная, на стороне ClickHouse: dedup-ключ =
DeveloperId(единственный компонентORDER BY), версионирование —ReplacingMergeTree(UpsertedAt)(merge оставляет строку с максимальнымUpsertedAt). Порядок per-developer гарантирован ключом сообщения (key = Id), поэтому старая версия не может прийти после новой; отдельная таблица идемпотентности не используется (PG-транзакции у консюмера нет).
Входные данные / payload события
Ссылка на контракт: event-contract: developer-upserted. Дом и типы колонок — dimensions/developer.md, раздел «Columns»; ниже — только поля, которые консюмер реально читает, и маппинг.
| Поле payload | Тип | Обязательность | Как используется (маппинг) |
|---|---|---|---|
Id | Guid | Да | → DeveloperId (UUID, единственный ключ ORDER BY); Guid.Empty → poison (DLQ) |
Name | string | Да | → Name (подпись графиков ЛК) |
LegalName | string? | Нет | → LegalName (Nullable(String)) |
Status | string (enum DeveloperStatus) | Да | → Status (LowCardinality(String)): Draft/Published/Hidden; unknown — сохранить как есть + warning + метрика chessboard_sync_unknown_enum_total (на чтении трактуется как наиболее ограничивающий Hidden) |
LogoFileId | Guid? | Нет | → LogoFileId (Nullable(UUID)) |
Version | long | Да | → SourceVersion (монотонный guard, диагностика гонок) |
FoundedYear, Highlights | — | — | не реплицируются (в dim_developer не нужны) |
Колонки, заполняемые самим консюмером: UpsertedAt = время события (версия ReplacingMergeTree).
Алгоритм
Консюмер батчевый: шаги 1–3 — на сообщение, шаг 4 — на батч (flush по порогу ≤ 1000 строк или 5 секунд; freshness SLA измерения ≤ 5 мин выполняется с запасом).
- Проверить контекст события: сверить
ContractVersion(=1; при несовпадении — warning-лог и продолжить, warn-and-proceed). Дубль на входе не отсеивается — дедуп декларативен (движок + порядок сообщений по ключу). - Проверить payload (по колонке «Обязательность»): ошибка десериализации или
Id == Guid.Empty→ DLQ (poison) + error-лог. У верхнего измерения родителей нет — snapshot самодостаточен, проверок связей нет. - Маппинг поле-в-поле: построить строку
dim_developerпо таблице выше (UpsertedAt= время события,SourceVersion = Version); переходов состояний нет. Добавить в буфер батча; внутрибатчевая свёртка поDeveloperId— оставить строку с максимальнымUpsertedAt(страховка от дублей в одном батче до merge). - Пакетно вставить (upsert) буфер строк в
pin_dev_stats.dim_developer. Логический upsert обеспечивает движокReplacingMergeTree(UpsertedAt)(merge схлопывает до строки с maxUpsertedAtperDeveloperId); точечное обновление/удаление не применяются. Смещение (offset) фиксируется строго после подтверждённой вставки батча; при ошибке — retry всего батча (идемпотентно), offset не двигается.
Диаграмма
Идемпотентность и replay
- Business key:
DeveloperId(dedup-ключORDER BY); версия-precedence —UpsertedAt(ReplacingMergeTree). Поведение дубля: строка перезаписывается идемпотентно, до merge чтение дубль-устойчиво —argMax(Name, UpsertedAt) … GROUP BY DeveloperId(FINALв online запрещён, правило SDK). - Version guard (по dimensions/developer.md) реализован декларативно:
per-key-ordering +
ReplacingMergeTree(UpsertedAt)+ внутрибатчевая свёртка — синхронногоevent.Version > SourceVersion-lookup нет (ADR-0056, batch-ingestion). - Replay/backfill: да — повтор топика
developer-upsertedс произвольного offset группойdev-stats-dim-developerбезопасен (upsert идемпотентен); первичное наполнение измерений — часть backfill при вводе сервиса (background/dimension-backfill.md).
Retry и DLQ
| Сбой | Retry | DLQ | Ручное действие |
|---|---|---|---|
| ClickHouse недоступен / timeout вставки | Да: retry батча, backoff (1с → 60с), offset не commit-ится | Нет (сбой инфраструктуры) | Алерт по росту ingest-lag > 15 мин; восстановить CH; консюмер догонит |
| Ошибка десериализации payload | Нет | Да (poison, per-message) | Разбор из DLQ; при системной причине — issue producer-у chessboard |
Id == Guid.Empty (шаг 2) | Нет | Да (poison) + error-лог | Проверить контракт producer-а; после фикса — replay из DLQ |
Версия события ≠ ContractVersion | Не сбой: warning-лог, продолжить | Нет | Сверить график миграции контракта с chessboard |
| Сбой между вставкой и commit offset | Повторная доставка батча (at-least-once) | Нет | Не требуется — дубль схлопывает ReplacingMergeTree |
Наблюдаемость и безопасность
- Logs: consumed (offset-диапазон, размер батча), skipped-poison (причина +
Id), flushed (строк, длительность вставки), failed (ошибка CH), unknown-enum (Status). Число дублей не логируется — dedup на стороне CH. - Metrics:
dev_stats_dim_developer_upserted_total,dev_stats_dim_ingest_lag_seconds(CreatedAt → вставка),dev_stats_dim_ingest_batch_size,chessboard_sync_unknown_enum_total, счётчики retry/DLQ. - Traces: consume span + insert span; корреляция из метаданных события.
- PII/masking: PII отсутствует (застройщик — организация; контакты/
Highlightsне реплицируются). Tenant:DeveloperId=TenantIdконтекста (ADR-0052); консюмер платформенный, scope-фильтрация — на чтении в API.
Кросс-сервисные контракты
| Направление | Контракт |
|---|---|
Ожидает от chessboard | Событие developer-upserted (pin.chessboard.developer-upserted.v1, ContractVersion = 1, at-least-once). Гарантии: ключ сообщения = Id (per-developer ordering — старая версия не приходит после новой), монотонная Version, самодостаточный snapshot (у верхнего измерения родителей нет). Id = AggregateId, CreatedAt — время snapshot-а. |
| Предоставляет | Строку dim_developer (ReplacingMergeTree, версия по UpsertedAt) с Name/LegalName/Status/LogoFileId. Потребители реплики: read API dev-stats (подписи графиков ЛК, подтверждение существования застройщика для scope DeveloperId = актор). В обогащении фактов не участвует напрямую — DeveloperId уже денормализован в dim_property_object. |
| Окно eventual-consistency | ≤ 5 мин (event → строка в измерении). Пока реплика отстаёт, подпись графика может показать прежнее имя застройщика; на резолв иерархии факта это не влияет. |
Предусловия и постусловия
- Предусловия: миграции DDL измерения применены; событие валидно (
Id != Guid.Empty). - Постусловия: в
dim_developerдляDeveloperIdактуальна строка с максимальнымUpsertedAt(merge схлопывает дубли); offset группыdev-stats-dim-developerпродвинут строго после подтверждённой вставки; никакие операционные данныеchessboardне изменены (реплика только для чтения).
Acceptance Criteria
- Happy path (создание): Дано — миграции применены; Когда — приходит
developer-upserted(D1,Name = "Стройтрест"); Тогда —argMax(Name, UpsertedAt) … GROUP BY DeveloperIdдляD1= "Стройтрест". - Обновление (новая версия): Дано — строка
D1есть; Когда — приходит событие с большимUpsertedAtи новымName; Тогда — чтение отдаёт новое имя (старая версия схлопывается при merge). - Идемпотентность (повтор доставки): Дано — событие
D1уже применено; Когда — повторная доставка; Тогда — логических дублей нет (ReplacingMergeTree+ внутрибатчевая свёртка), offset продвинут. - Poison: Дано —
Id == Guid.Emptyили битый payload; Когда — обработка; Тогда — сообщение в DLQ (poison) + error-лог, партиция продолжается. - Unknown-enum
Status: Дано —Statusвне известного набора; Когда — обработка; Тогда — значение сохраняется как есть + warning + метрикаchessboard_sync_unknown_enum_total; на чтении трактуется как наиболее ограничивающийHidden. - Отказ хранилища: Дано — ClickHouse недоступен; Когда — batch-вставка падает; Тогда — offset не коммитится, retry с backoff, догон без дублей.
Обратные ссылки
Автоссылки: где используется этот документ.
- События (2): События dev-stats, Event Contract: pin.chessboard.developer-upserted (DeveloperUpsertedKafkaEvent)
- Консюмеры (5): Consumer: DevStatsBuildingUpsertedConsumer, Consumer: DevStatsLayoutGroupUpsertedConsumer, Consumer: DevStatsLayoutUpsertedConsumer, Consumer: DevStatsProjectUpsertedConsumer, Consumer: DevStatsPropertyObjectUpsertedConsumer
- index.md (1): DevStats Service (Pin.DevStats)
Связанные документы
- Дом колонок и dedup: dimensions/developer.md; контракт события: chessboard/events/developer-upserted.md.
- Соседние ingest-консюмеры измерений: project-upserted.md, building-upserted.md, layout-group-upserted.md, layout-upserted.md, property-object-upserted.md.
- Обзор сервиса: ../README.md; первичная загрузка измерений: ../background/dimension-backfill.md.
- ADR-0052 (tenancy), ADR-0056 (simplicity-first), ADR-0057 (dev-stats измерения).