Перейти к основному содержимому

UC-CHS-005. Синхронизация реплики из Talent Chessboard (system)

Назначение

Системный сценарий репликации: talent.microservices.chessboard публикует события застройщиков, проектов, зданий, объектов недвижимости, групп и планировок; фоновая подписка шахматки создаёт/обновляет гибридные сущности (реплика-поля Talent обновляются, PIN-поля нетронуты), защищается от событий в неверном порядке (ReplicaVersion) и заново публикует собственные события pin.chessboard.* для потребителей PIN. Сценарий относится к контексту chessboard.

Metadata

ПолеЗначение
IDUC-CHS-005
Контекстchessboard (см. docs/06-services/chessboard/)
Типосновной
Статусdraft

Актор

ПолеЗначение
РольSystem (фоновая подписка синка, consumer-группа pin-chessboard-talent-sync)
Permissions / scopePermissions не применяются: 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)

Основной Поток

  1. Talent публикует snapshot-событие агрегата: EventId, OccurredAt, CorrelationId и payload c Id, полями и Version.
  2. Синк обрабатывает событие единым шаблоном для всех шести агрегатов (ниже — на примере объекта недвижимости → PropertyObjectEntity):
    • Идемпотентность. Повторная доставка того же EventId → ack без эффекта.
    • Маппинг enum Talent → PIN c fallback-ами (reference-data.md): unknown windowViews-значение → Street, unknown layout.schemas.kindPlan2D, unknown status → см. 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). Статус Talent Sold при локальном Free → переход Free → Sold (StatusSource = TalentSync) — продажа вне PIN (INV-CHS-001). Статус Talent НЕ трогает локальные Reserved/Booked (холды владеет CRM — конфликт см. A6).
    • Публикация. В одном согласованном изменении с мутацией заново публикуется PropertyObjectUpsertedKafkaEventpin.chessboard.property-object-upserted.v1 (+ PropertyObjectPriceChangedKafkaEvent при смене цены, PropertyObjectStatusChangedKafkaEvent при Free → Sold); downstream-контексты PIN на топики Talent не подписываются.
    • Результат. Ack offset-а.
  3. Аналогично по уровням иерархии: Developer (создание при отсутствии — Status = Draft; публикация профиля — решение PIN, синк не публикует автоматически; INV-DEV-1..4) → Project (Status = Draft при создании; enum-фоллбеки ProjectType → ResidentialComplex, ProjectClass → Comfort, ConstructionStatus → UnderConstruction) → BuildingLayoutGroupLayoutPropertyObject.
  4. Каждый применённый snapshot обновляет ReplicaVersion/LastSyncedAt реплики — мониторинг лага chessboard_talent_sync_lag_seconds = now − max(LastSyncedAt); алерт при > 5 мин (README SLO).
  5. Потребители PIN (catalog, dev-stats, rewards, invest-analytics) получают события pin.chessboard.*-upserted.v1 и обновляют свои реплики/проекции.

Альтернативные Потоки

A1. Событие с устаревшей версией (out-of-order)

  1. payload.Version <= ReplicaVersion → идемпотентный skip, метрика chessboard_sync_stale_events_total, ack. Состояние не меняется.

A2. Повторная доставка (at-least-once)

  1. Тот же EventId уже обработан → ack без эффекта.

A3. Дочернее событие раньше родительского

  1. Родитель по ExternalTalent<Parent>Id не найден → retriable fail CHESSBOARD_SYNC_PARENT_NOT_FOUND; retry с exponential backoff → DLQ (событие не теряется). После применения родительского события replay проходит.

A4. Локальные PIN-правки vs snapshot

  1. Snapshot применяет только реплика-поля; PIN-поля (Description, Financing, LegalInfo, Highlights, статусы публикации PIN, таймеры CRM) не перезаписываются (инварианты *-SyncOwnedFields: INV-DEV-3, INV-PRJ-9, INV-BLD-7, INV-LAY-6). Конфликтов «последний пишет» между синком и API нет — наборы полей не пересекаются.

A5. Неизвестный статус объекта Talent

  1. Статусы Talent маппятся: доступен → Free, продан → Sold, скрыт/снят → Hidden; неизвестное значение → событие в DLQ + алерт (статус — критичное поле, молчаливый fallback недопустим в отличие от декоративных enum-ов шага 2).

A6. Talent прислал продажу объекта недвижимости под активным холдом PIN

  1. Snapshot status = Sold, локально Reserved/Booked (активная сделка CRM): переход НЕ применяется (INV-CHS-001 разрешает TalentSync только Free → Sold); событие → DLQ + алерт CHESSBOARD_SYNC_HOLD_CONFLICT + тикет заботы (двойная продажа — ручной разбор: release сделки CRM либо отмена продажи Talent). Паспортные поля snapshot-а при этом применяются.

A7. Пауза синка (инцидент источника)

  1. chessboard.talent-sync.enabled = false → потребление останавливается (pause), offset не двигается; после включения — догоняет без потерь. Локальные данные и API работают.

A8. Начальная загрузка (bootstrap)

  1. Первичное наполнение — чтение топиков 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 не применяется

Сценарий полностью событийный: HTTP-кодов нет, «клиент» ошибок — retry/DLQ-политика синка.

Доменные События

СобытиеКогда поднимаетсяSubscribers
DeveloperCreatedEvent / ProjectCreatedEvent / BuildingCreatedEvent / LayoutCreatedEvent / LayoutGroupCreatedEvent / PropertyObjectCreatedEventСоздание репликиin-proc
PropertyObjectStatusChangedEventFree → Sold синкомin-proc → ProjectAggregatesRecalcRequestedEvent
PropertyObjectPriceChangedEventСмена цены из snapshotin-proc → recalc
ProjectAggregatesRecalcRequestedEventИзменения объектов недвижимости синкомjob project-aggregates-recalc

Kafka Интеграционные События

СобытиеTopicКогда публикуетсяConsumers
DeveloperUpsertedKafkaEvent.v1pin.chessboard.developer-upserted.v1Каждый применённый snapshot застройщикаcatalog, dev-stats, crm, rewards
ProjectUpsertedKafkaEvent.v1pin.chessboard.project-upserted.v1Каждый применённый snapshot проектаcatalog, dev-stats, identity, news
BuildingUpsertedKafkaEvent.v1pin.chessboard.building-upserted.v1Каждый применённый snapshot зданияcatalog, dev-stats
LayoutGroupUpsertedKafkaEvent.v1 / LayoutUpsertedKafkaEvent.v1pin.chessboard.layout-group-upserted.v1 / pin.chessboard.layout-upserted.v1Snapshot группы/планировкиdev-stats, rewards, invest-analytics, catalog
PropertyObjectUpsertedKafkaEvent.v1pin.chessboard.property-object-upserted.v1Каждый применённый snapshot объектаcatalog, dev-stats, rewards
PropertyObjectStatusChangedKafkaEvent.v1pin.chessboard.property-object-status-changed.v1Free → Sold синкомcatalog, favorites, selections, notifications
PropertyObjectPriceChangedKafkaEvent.v1pin.chessboard.property-object-price-changed.v1Цена из snapshot изменила Priceinvest-analytics, catalog

Acceptance Criteria

#КритерийПроверка
AC1Повторная доставка и out-of-order события не меняют состояние (A1, A2)тесты двойной доставки и stale-версии
AC2Синк не перетирает PIN-поля ни одного агрегата (A4)тест: локальная правка + snapshot по каждой сущности
AC3Дочернее событие до родителя → retry, после — применяется (A3)тест порядка объект недвижимости → building
AC4Unknown декоративный 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 минпроверка метрик/алертов

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы