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

UC-HND-002. Система синхронизирует реплику справочника из Talent

Назначение

Системный (событийный) сценарий: PIN не владеет справочными данными (страны, регионы, города, валюты, языки) — их источник истины внешний, talent.microservices.handbook. Синк потребляет upsert-события источника и поддерживает в контексте handbook локальную реплику пяти справочников, а каждое применённое изменение re-publish-ит собственным событием PIN в compacted-топики pin.handbook.*.v1, чтобы downstream-сервисы PIN (chessboard и будущие потребители) зависели только от PIN, а не подписывались на Talent напрямую. Ключевые особенности: у событий источника нет per-record версии — порядок применения задаётся временем публикации (CreatedAt envelope) и порядком партиции; синк идемпотентен к повторам; источник не эмитит delete-события (delete-gap) — физических удалений в PIN нет вообще, устаревшие записи выявляет reconciliation после полной пересинхронизации, а решение об архиве принимает администратор (UC-HND-001). Связан с контекстом handbook.

Metadata

ПолеЗначение
IDUC-HND-002
Контекстhandbook (см. docs/06-services/handbook/)
Типосновной (+ A1 устаревшее/повторное событие, A2 city раньше country, A3 auto-restore архивной записи, A4 ISO-конфликт, A5 гео-рассогласование, A6 полная пересинхронизация + reconciliation)
Статусdraft

Актор

ПолеЗначение
РольSystem (Kafka-консюмер синка справочника, consumer group pin-handbook-talent-sync)
Permissions / scopesystem-scope консюмера; доступ к топикам Talent — m2m ACL Kafka
Tenantне применяется — справочники платформенные (не tenanted), PII нет
Record-levelне применяется

Предусловие

  • Топики источника talent.microservices.handbook доступны (страны, города+регион, валюты, языки); пятый поток источника — контактная информация — вне scope v1 и не потребляется.
  • Consumer group pin-handbook-talent-sync работает; целевые реплики и чекпоинты синка (HandbookSyncState) существуют или создаются первым событием записи.
  • Downstream-сервисы PIN подписаны на pin.handbook.*.v1, а не на топики Talent.

Триггер

ПолеЗначение
ИсточникKafka event
Триггерящий элементConsumer group pin-handbook-talent-sync (talent-handbook-sync.md): upsert-событие страны / города (+ вложенный регион) / валюты / языка; key = id записи (per-record порядок в партиции)

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

Дом алгоритма/маппинга — консюмер talent-handbook-sync.md; ниже — поток на уровне use-case (единая рамка для всех четырёх потоков; поток «город» дополнительно derive-ит регион).

  1. Приём и контекст. Консюмер получает upsert-событие записи; восстанавливает метаданные: MessageId (для дедупликации), envelope CreatedAt (время публикации источником — источник монотонности, т.к. per-record версии в payload нет), ContractVersion (ожидается 2; при несовпадении — warn-and-proceed + метрика), id записи (key).
  2. Идемпотентность по сообщению. По ключу handbook-sync:{тип}:{MessageId} проверяется, обработано ли сообщение: дубль → пропуск (skipped-duplicate, ack), состояние не меняется.
  3. Проверка входа. Обязательные поля snapshot-а присутствуют и нормализованы (ISO-коды к нужному регистру, имена — truncate + warn при переполнении); envelope CreatedAt задан. Существование записи не проверяется — операция upsert.
  4. Монотонный guard (упорядочение по CreatedAt). Если запись уже есть и envelope CreatedAt <= LastEventAt — событие устаревшее/повтор пересинхронизации: skipped-stale (A1), при этом обновляется только отметка живости чекпоинта (LastEventId/LastEventAt) — основа reconciliation, — а реплика-поля не трогаются. Так поздний повтор не откатывает более свежий snapshot.
  5. Доменные guard-ы и решения.
    • Зависимость country→city (только поток города): страна по ссылке из city-события должна существовать; нет → retriable-ожидание (A2) — страна придёт своим событием, city-событие не теряется.
    • ISO-уникальность: ISO-коды не заняты другой неархивной записью (INV-HND-2); конфликт → отказ и DLQ (A4).
    • Гео-согласованность (регион/город): страна региона берётся из страны вмещающего city-события; смена страны региона не поддерживается — нарушение → DLQ (A5).
    • Auto-restore: если запись была заархивирована в PIN, но пришла из источника — архив снимается (IsArchived = false) + warn-метрика: источник главнее локального архива (A3).
  6. Upsert реплики (snapshot-семантика). Синк-владеемые поля перезаписываются целиком по snapshot-у источника; LastEventAt = envelope CreatedAt. Для потока города дополнительно derive-ится регион из вложенной модели (создание/обновление записи региона под собственным монотонным guard-ом — независимо от города).
  7. Re-publish события PIN. В том же согласованном изменении публикуется {тип}-upserted в compacted-топик pin.handbook.{countries|regions|cities|currencies|languages}.v1 (key = id, полный snapshot реплики + IsArchived + LastEventAt). Для города регион re-publish-ится только при фактическом изменении записи региона (дедупликация шторма одинаковых регионов от разных городов).
  8. Согласованность и ack. Реплика, чекпоинт HandbookSyncState (Status = Synced), отметка идемпотентности и исходящие события фиксируются атомарно; offset подтверждается только после фиксации. Downstream-сервисы PIN получают pin.handbook.* и обновляют свои локальные реплики (секунды eventual).

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

примечание
A1. Устаревшее / повторное событие (упорядочение по CreatedAt)
  1. Событие с envelope CreatedAt <= LastEventAt существующей записи (out-of-order доставка или повтор пересинхронизации) → реплика-поля не меняются, skipped-stale + метрика; обновляется отметка живости чекпоинта (LastEventId/LastEventAt). Ack. Так порядок применения не зависит от порядка доставки, а определяется временем публикации источником.
A2. City-событие обогнало country
  1. Страна по ссылке города ещё не создана (country- и city-топики взаимно не упорядочены) → retriable-отказ HANDBOOK_SYNC_COUNTRY_NOT_FOUND; ретрай с backoff (несколько попыток). После прихода country-события повтор успешен — город появится с запозданием (окно eventual). Исчерпание попыток → DLQ + алерт (разбор: пришло ли country-событие / нужна ли пересинхронизация).
A3. Auto-restore архивной записи
  1. Запись была заархивирована администратором в PIN (UC-HND-001), но пришла upsert-событием из источника (не была удалена в источнике / заведена заново) → синк снимает архив (IsArchived = false) + warn-метрика (источник главнее локального архива); реплика-поля перезаписываются; re-publish события. Если скрытие значения всё же нужно — вопрос решается правкой данных в Talent, а не повторным архивом в PIN.
A4. ISO-конфликт
  1. ISO-код записи занят другой неархивной записью того же справочника (INV-HND-2) → non-retriable-отказ HANDBOOK_SYNC_ISO_CONFLICT → DLQ + алерт + HandbookSyncState.Status = Conflict. Автопочинки в PIN нет: разрешение (какая запись легитимна) — правкой данных в источнике, затем replay из DLQ.
A5. Гео-рассогласование региона
  1. Страна вмещающего city-события не совпадает со страной существующей записи региона (попытка «переноса» региона между странами, не поддерживается, INV-HND-3) → non-retriable HANDBOOK_SYNC_GEO_MISMATCH → DLQ + алерт. Разрешение — правка данных в источнике + replay.
A6. Полная пересинхронизация и delete-gap (reconciliation)
  1. Источник не эмитит delete-события — запись, удалённая в Talent, продолжает жить в реплике PIN (delete-gap). Периодически/по требованию проводится полная пересинхронизация (повторный проход всех топиков источника): upsert + monotonic-guard превращают повторы в no-op — реплика сходится к текущему snapshot-у источника без искажений.
  2. По итогам ресинка формируется reconciliation-отчёт: строки реплики, не пришедшие в окно ресинка, помечаются кандидатами в архив. Автоматического архива по отсутствию нет — синк только выявляет расхождение; вывод записи из оборота — ручное решение администратора (UC-HND-001, talent-initial-import.md). Так delete-gap источника компенсируется без риска потерять валидную запись.

Постусловие

  • Целевая реплика записи отражает последний snapshot источника (синк-владеемые поля перезаписаны; LastEventAt = envelope CreatedAt; при auto-restore архив снят); чекпоинт HandbookSyncState = Synced (либо Conflict/DeadLettered при A4/A5/исчерпании ретраев).
  • Для потока города при наличии региона — запись региона derive-нута и согласована.
  • Опубликованы соответствующие pin.handbook.*-события; локальные реплики downstream (chessboard) согласуются асинхронно (секунды). Устаревшее/дублирующее событие состояние реплики не изменило (skip), отметка живости обновлена.
  • Физических удалений не выполнялось; delete-gap источника компенсируется reconciliation + ручным архивом.

Возможные Ошибки

Сценарий системный (консюмер) — пользовательских HTTP-ошибок нет. Классы ошибок и политика retry/DLQ — дом talent-handbook-sync.md:

УсловиеКодКлассRetry / DLQ
Страна не найдена (city раньше country, A2)HANDBOOK_SYNC_COUNTRY_NOT_FOUNDretriablebackoff-ретрай; после исчерпания — DLQ + алерт
ISO-код занят другой неархивной записью (A4)HANDBOOK_SYNC_ISO_CONFLICTnon-retriableDLQ + алерт; Status = Conflict; разрешение в источнике
Смена страны региона (A5)HANDBOOK_SYNC_GEO_MISMATCHnon-retriableDLQ + алерт; Status = Conflict; разрешение в источнике
Инфраструктурный (PostgreSQL/шина недоступны)retriableбесконечный backoff, событие не теряется
Гонка с admin archive/restoreконкурентная3 немедленных попытки (перечитать запись)
ContractVersion mismatch (источник 2→3)warn-and-proceedобработка продолжается; warn-метрика

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

СобытиеTopicКогда публикуетсяConsumers
country-upsertedpin.handbook.countries.v1 (compacted, key = id, ContractVersion 1)Применён upsert страны (шаг 7)chessboard (локальная реплика), будущие потребители PIN
region-upsertedpin.handbook.regions.v1 (compacted)Фактическое изменение derive-нутого региона (создание/смена Name/restore)chessboard, будущие потребители
city-upsertedpin.handbook.cities.v1 (compacted)Применён upsert городаchessboard, будущие потребители
currency-upsertedpin.handbook.currencies.v1 (compacted)Применён upsert валютыchessboard, будущие потребители
language-upsertedpin.handbook.languages.v1 (compacted)Применён upsert языкабудущие потребители (текущих подписчиков нет)

Потребляемые события — контракты источника (talent.microservices.handbook, schema registry Talent); DLQ группы — pin.handbook.talent-sync-dlq.v1.

Acceptance Criteria

#КритерийПроверка
AC1Upsert-событие страны создаёт/обновляет реплику по snapshot-семантике, ставит LastEventAt = envelope CreatedAt, публикует country-upserted (snapshot + IsArchived); чекпоинт Syncedинтеграционный тест потока country
AC2Порядок применения — по envelope CreatedAt: событие с CreatedAt ≤ LastEventAt не откатывает реплику (skipped-stale), обновляет только отметку живости (A1)тест монотонности (out-of-order доставка)
AC3Повторная доставка того же MessageIdskipped-duplicate, состояние не меняется, повторное событие не публикуетсятест идемпотентности
AC4City-событие раньше country → HANDBOOK_SYNC_COUNTRY_NOT_FOUND, retry; после country повтор успешен, город появляется (A2)тест out-of-order country→city
AC5Город с регионом derive-ит запись региона (страна региона = страна города); region-upserted публикуется только при фактическом изменении регионатест derive региона
AC6Upsert поверх архивной записи снимает архив (IsArchived = false) + warn-метрика, публикует событие (A3, auto-restore)тест auto-restore
AC7ISO-конфликт → DLQ + Status = Conflict, реплика не изменена (A4); смена страны региона → DLQ GEO_MISMATCH (A5)негативные тесты guard-ов
AC8Delete-gap: удалённая в источнике запись живёт в реплике; полная пересинхронизация сходится (повторы no-op), reconciliation-отчёт помечает кандидатов в архив; автоархива нет (A6)тест ресинка + reconciliation
AC9Downstream PIN зависят только от pin.handbook.* — на топики Talent не подписаны; событие несёт полный snapshot + IsArchivedконтракт-тест исходящих событий

Наблюдаемость

  • Логи (без PII). consumed / skipped-duplicate / skipped-stale / created / updated / auto-restored / conflict / dead-lettered с MessageId, типом записи, ExternalId, envelope CreatedAt, LastEventAt. В справочных данных PII нет.
  • Метрики. handbook_sync_lag_seconds (момент применения − envelope CreatedAt), handbook_sync_stale_events_total, handbook_sync_dlq_total, handbook_sync_auto_restores_total, handbook_sync_contract_mismatch_total, handbook_sync_retries_total.
  • Alerts. DLQ > 0; Status = Conflict > 0; лаг применения > 15 мин при ненулевом потоке; contract_mismatch > 0 (сигнал смены контракта источника).

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

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

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