UC-CHS-005. Синхронизация реплики из Talent Chessboard (system)
Назначение
Системный сценарий репликации: talent.microservices.chessboard публикует события застройщиков,
проектов, зданий, объектов недвижимости, групп и планировок; фоновая подписка шахматки создаёт/обновляет
гибридные сущности (реплика-поля Talent обновляются, PIN-поля нетронуты), защищается от событий в
неверном порядке (ReplicaVersion) и заново публикует собственные события pin.chessboard.* для
потребителей PIN. Сценарий относится к контексту chessboard.
Metadata
| Поле | Значение |
|---|---|
| ID | UC-CHS-005 |
| Контекст | chessboard (см. docs/06-services/chessboard/) |
| Тип | основной |
| Статус | draft |
Актор
| Поле | Значение |
|---|---|
| Роль | System (фоновая подписка синка, consumer-группа pin-chessboard-talent-sync) |
| Permissions / scope | Permissions не применяются: m2m ACL Kafka на топики talent.chessboard.*; мутации — под system-actor (CreatedByUserId = system-talent-sync) |
| Tenant | Кросс-tenant: реплики создаются с TenantId соответствующего застройщика (ADR-0052) |
| Record-level | Не применяется |
Предусловие
- Kafka доступна; синк подписан на
talent.chessboard.developers.v1,talent.chessboard.projects.v1,talent.chessboard.buildings.v1,talent.chessboard.layout-groups.v1,talent.chessboard.layouts.v1,talent.chessboard.property-objects.v1(имена топиков и модели — assumption до подтверждения schema registry Talent; README «Известные Ограничения»). - Feature flag
chessboard.talent-sync.enabled = true(пауза при инцидентах источника). - Для дочерних событий — родительская реплика уже применена (иначе retry — A3).
Триггер
| Поле | Значение |
|---|---|
| Источник | Kafka event |
| Триггерящий элемент | Snapshot-события Talent по каждому из шести агрегатов (developer / project / building / layout-group / layout / property-object) |
Основной Поток
- Talent публикует snapshot-событие агрегата:
EventId,OccurredAt,CorrelationIdи payload cId, полями иVersion. - Синк обрабатывает событие единым шаблоном для всех шести агрегатов (ниже — на примере объекта
недвижимости →
PropertyObjectEntity):- Идемпотентность. Повторная доставка того же
EventId→ ack без эффекта. - Маппинг enum Talent → PIN c fallback-ами (reference-data.md):
unknown
windowViews-значение →Street, unknownlayout.schemas.kind→Plan2D, unknownstatus→ см. A5. Каждый fallback — warning-лог + метрикаchessboard_sync_unknown_enum_total{enum,value}, синк не падает. - Version-guard. Событие применяется только если
payload.Version > propertyObject.ReplicaVersion, иначе идемпотентный пропуск (INV-PRJ-5 / INV-BLD-5 / INV-LAY-4 / INV-LGR-4 / INV-DEV-4 + аналог для объекта недвижимости) + метрикаchessboard_sync_stale_events_total. - Поиск реплики и родителя. Реплика ищется по внешнему ключу
ExternalTalentPropertyObjectId = payload.Id; родитель — поExternalTalentBuildingId; при привязке планировки — поExternalTalentLayoutId. Родитель не найден → retriable fail (A3). - Применение snapshot. Реплики нет → создаётся объект (
Id = payload.Id,ProjectId/BuildingId/TenantIdиз родителя,Status= маппинг статуса Talent (Free/Sold/Hidden),StatusSource = TalentSync,SyncSource = TalentChessboard,ReplicaVersion = payload.Version,LastSyncedAt = OccurredAt; поля из snapshot:Number,Floor,Section,Rooms,TotalArea = layout.square,LivingArea = layout.livingSquare,Price,WindowViews,Capacity,LayoutId). Реплика есть → перезаписываются только реплика-поля (Number,Floor,Section,Rooms, площади,WindowViews, привязкаLayoutId); PIN-поля (Description, локальные правки цены, статусные таймеры CRM) неприкосновенны — инварианты*-SyncOwnedFields. - Спец-правила объекта недвижимости. Смена цены в snapshot → применяется как смена цены с
Source = TalentSync(публикует price-событие — UC-CHS-003 A3). Статус TalentSoldпри локальномFree→ переходFree → Sold(StatusSource = TalentSync) — продажа вне PIN (INV-CHS-001). Статус Talent НЕ трогает локальныеReserved/Booked(холды владеет CRM — конфликт см. A6). - Публикация. В одном согласованном изменении с мутацией заново публикуется
PropertyObjectUpsertedKafkaEvent→pin.chessboard.property-object-upserted.v1(+PropertyObjectPriceChangedKafkaEventпри смене цены,PropertyObjectStatusChangedKafkaEventприFree → Sold); downstream-контексты PIN на топики Talent не подписываются. - Результат. Ack offset-а.
- Идемпотентность. Повторная доставка того же
- Аналогично по уровням иерархии:
Developer(создание при отсутствии —Status = Draft; публикация профиля — решение PIN, синк не публикует автоматически; INV-DEV-1..4) →Project(Status = Draftпри создании; enum-фоллбекиProjectType → ResidentialComplex,ProjectClass → Comfort,ConstructionStatus → UnderConstruction) →Building→LayoutGroup→Layout→PropertyObject. - Каждый применённый snapshot обновляет
ReplicaVersion/LastSyncedAtреплики — мониторинг лагаchessboard_talent_sync_lag_seconds = now − max(LastSyncedAt); алерт при > 5 мин (README SLO). - Потребители PIN (
catalog,dev-stats,rewards,invest-analytics) получают событияpin.chessboard.*-upserted.v1и обновляют свои реплики/проекции.
Альтернативные Потоки
A1. Событие с устаревшей версией (out-of-order)
payload.Version <= ReplicaVersion→ идемпотентный skip, метрикаchessboard_sync_stale_events_total, ack. Состояние не меняется.
A2. Повторная доставка (at-least-once)
- Тот же
EventIdуже обработан → ack без эффекта.
A3. Дочернее событие раньше родительского
- Родитель по
ExternalTalent<Parent>Idне найден → retriable failCHESSBOARD_SYNC_PARENT_NOT_FOUND; retry с exponential backoff → DLQ (событие не теряется). После применения родительского события replay проходит.
A4. Локальные PIN-правки vs snapshot
- Snapshot применяет только реплика-поля; PIN-поля (
Description,Financing,LegalInfo,Highlights, статусы публикации PIN, таймеры CRM) не перезаписываются (инварианты*-SyncOwnedFields: INV-DEV-3, INV-PRJ-9, INV-BLD-7, INV-LAY-6). Конфликтов «последний пишет» между синком и API нет — наборы полей не пересекаются.
A5. Неизвестный статус объекта Talent
- Статусы Talent маппятся: доступен →
Free, продан →Sold, скрыт/снят →Hidden; неизвестное значение → событие в DLQ + алерт (статус — критичное поле, молчаливый fallback недопустим в отличие от декоративных enum-ов шага 2).
A6. Talent прислал продажу объекта недвижимости под активным холдом PIN
- Snapshot
status = Sold, локальноReserved/Booked(активная сделка CRM): переход НЕ применяется (INV-CHS-001 разрешаетTalentSyncтолькоFree → Sold); событие → DLQ + алертCHESSBOARD_SYNC_HOLD_CONFLICT+ тикет заботы (двойная продажа — ручной разбор: release сделки CRM либо отмена продажи Talent). Паспортные поля snapshot-а при этом применяются.
A7. Пауза синка (инцидент источника)
chessboard.talent-sync.enabled = false→ потребление останавливается (pause), offset не двигается; после включения — догоняет без потерь. Локальные данные и API работают.
A8. Начальная загрузка (bootstrap)
- Первичное наполнение — чтение топиков Talent с offset 0 (если compacted) либо разовый импорт по API Talent (property-objects list/by-ids) миграционным job-ом talent-initial-import; далее — только события.
Кросс-сервисный контракт (Talent → синк → PIN)
Что синк ожидает от Talent (входные гарантии).
- Snapshot-семантика: каждое событие несёт полное актуальное состояние агрегата (
Id, поля, монотонно растущийVersionв пределах одногоId), а не дельту — поэтому пропуск устаревших версий безопасен (version-guard). EventIdстабилен и уникален на факт публикации — ключ идемпотентности при at-least-once доставке.- Порядок «родитель раньше ребёнка» между разными агрегатами не гарантирован — отсюда retriable-обработка «родитель не найден» (A3), а не отказ.
- Имена топиков и модели полей — assumption до подтверждения schema registry Talent (README «Известные Ограничения»); расхождение схемы или неизвестный статус → DLQ + алерт (A5), а не молчаливая потеря.
Что синк предоставляет PIN (исходящие гарантии).
- Повторная публикация
pin.chessboard.*-upserted.v1(+property-object-price-changed.v1,property-object-status-changed.v1) — key =Idсоответствующего агрегата,ContractVersion = 1, at-least-once; downstream PIN на топики Talent не подписывается (репликация изолирована в шахматке). - Упорядоченность в пределах ключа: события одного объекта недвижимости/проекта доставляются в порядке применения snapshot-ов; между разными агрегатами порядок независим.
- Публикация — в одном согласованном изменении с мутацией реплики (AC6): либо и реплика обновлена, и событие ушло, либо ни то, ни другое.
- Потребители PIN обязаны быть идемпотентны по (
Id,Version) — повтор/replay не должен задваивать эффект.
Поведение при отсутствии/устаревании реплик. Дочерний агрегат без родителя → retry→DLQ→replay (A3), не
частичная запись. Устаревшее событие (Version <= ReplicaVersion) → идемпотентный skip (A1). Лаг синка
виден метрикой chessboard_talent_sync_lag_seconds; при > 5 мин — алерт (downstream может отставать на это
окно). Пауза feature-flag (A7) останавливает потребление без потери offset-ов — реплики «замораживаются» на
последнем применённом состоянии, локальные данные и API продолжают работать.
Постусловие
- Реплики шести агрегатов актуальны snapshot-ам Talent:
SyncSource = TalentChessboard,ReplicaVersion/LastSyncedAtобновлены; PIN-поля нетронуты. - Продажи вне PIN отражены (
Free → Sold,StatusSource = TalentSync); активные холды CRM не затронуты. - Downstream-контексты получили повторную публикацию
pin.chessboard.*-upserted.v1(+ price/status события объектов недвижимости);invest-analyticsполучил ценовые точки Talent-источника. - Метрики лага/стейла/unknown-enum актуальны; DLQ пуст (или алерт).
Возможные Ошибки
| Код | HTTP | Условие | Поведение клиента |
|---|---|---|---|
CHESSBOARD_SYNC_PARENT_NOT_FOUND | — (retriable) | Дочернее событие раньше родительского (A3) | retry backoff → DLQ + алерт |
CHESSBOARD_SYNC_UNKNOWN_STATUS | — (DLQ) | Неизвестный статус объекта Talent (A5) | DLQ + алерт; маппинг чинится релизом |
CHESSBOARD_SYNC_HOLD_CONFLICT | — (DLQ) | Продажа Talent поверх активного холда PIN (A6) | DLQ + алерт + тикет заботы; runbook ручного release |
CHESSBOARD_DEVELOPER_TALENT_DUPLICATE | — (DLQ) | Нарушение unique ExternalTalentDeveloperId (INV-DEV-2) | DLQ; ручной разбор |
CHESSBOARD_PROPERTY_OBJECT_INVALID_STATUS_TRANSITION | — (DLQ) | Недопустимый переход из snapshot (INV-CHS-001) | DLQ; runbook пересинка проекта |
Сценарий полностью событийный: HTTP-кодов нет, «клиент» ошибок — retry/DLQ-политика синка.
Доменные События
| Событие | Когда поднимается | Subscribers |
|---|---|---|
DeveloperCreatedEvent / ProjectCreatedEvent / BuildingCreatedEvent / LayoutCreatedEvent / LayoutGroupCreatedEvent / PropertyObjectCreatedEvent | Создание реплики | in-proc |
PropertyObjectStatusChangedEvent | Free → Sold синком | in-proc → ProjectAggregatesRecalcRequestedEvent |
PropertyObjectPriceChangedEvent | Смена цены из snapshot | in-proc → recalc |
ProjectAggregatesRecalcRequestedEvent | Изменения объектов недвижимости синком | job project-aggregates-recalc |
Kafka Интеграционные События
| Событие | Topic | Когда публикуется | Consumers |
|---|---|---|---|
DeveloperUpsertedKafkaEvent.v1 | pin.chessboard.developer-upserted.v1 | Каждый применённый snapshot застройщика | catalog, dev-stats, crm, rewards |
ProjectUpsertedKafkaEvent.v1 | pin.chessboard.project-upserted.v1 | Каждый применённый snapshot проекта | catalog, dev-stats, identity, news |
BuildingUpsertedKafkaEvent.v1 | pin.chessboard.building-upserted.v1 | Каждый применённый snapshot здания | catalog, dev-stats |
LayoutGroupUpsertedKafkaEvent.v1 / LayoutUpsertedKafkaEvent.v1 | pin.chessboard.layout-group-upserted.v1 / pin.chessboard.layout-upserted.v1 | Snapshot группы/планировки | dev-stats, rewards, invest-analytics, catalog |
PropertyObjectUpsertedKafkaEvent.v1 | pin.chessboard.property-object-upserted.v1 | Каждый применённый snapshot объекта | catalog, dev-stats, rewards |
PropertyObjectStatusChangedKafkaEvent.v1 | pin.chessboard.property-object-status-changed.v1 | Free → Sold синком | catalog, favorites, selections, notifications |
PropertyObjectPriceChangedKafkaEvent.v1 | pin.chessboard.property-object-price-changed.v1 | Цена из snapshot изменила Price | invest-analytics, catalog |
Acceptance Criteria
| # | Критерий | Проверка |
|---|---|---|
| AC1 | Повторная доставка и out-of-order события не меняют состояние (A1, A2) | тесты двойной доставки и stale-версии |
| AC2 | Синк не перетирает PIN-поля ни одного агрегата (A4) | тест: локальная правка + snapshot по каждой сущности |
| AC3 | Дочернее событие до родителя → retry, после — применяется (A3) | тест порядка объект недвижимости → building |
| AC4 | Unknown декоративный enum → fallback + метрика, не DLQ; unknown статус → DLQ (A5) | тесты enum-маппинга |
| AC5 | Продажа Talent при Free применяется; при Reserved/Booked — DLQ + алерт (A6) | тест конфликтов статуса |
| AC6 | Каждый применённый snapshot заново публикует pin.chessboard.* в той же транзакции | outbox-тест |
| AC7 | Пауза feature-flag останавливает потребление без потери offset-ов (A7) | тест паузы/возобновления |
| AC8 | Лаг синка виден метрикой; алерт при > 5 мин | проверка метрик/алертов |
Обратные ссылки
Автоссылки: где используется этот документ.
- Фоновые задачи (1): Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)
- registries (1): Реестр use cases и user stories PIN
Связанные Документы
- Consumer детально: talent-chessboard-sync — маппинги полей всех шести агрегатов, retry/DLQ-политика.
- Domain entities: developer, project, building, property-object, layout, layout-group, reference-data (fallback-правила).
- Bootstrap: talent-initial-import.
- Аналог в identity: UC-IDN-007-talent-sync-company-users.
- Связанные UC: UC-CHS-006 (владение холдами CRM), UC-CHS-003 (ценовые события).