Event Contract: country-upserted (CountryUpsertedEvent)
Версионируемый контракт интеграционного события «запись страны справочника создана/изменена».
Consumer-документы ссылаются сюда, не переописывая payload. Дом полей —
entity: CountryEntity.
Назначение
Фиксирует каждое изменение состояния реплики страны в контексте handbook: применённый upsert синка
Talent, archive/restore администратора, повторную эмиссию при полной пересинхронизации. Событие публичное:
даёт downstream-сервисам PIN актуальную запись страны без синхронного запроса в handbook и без подписки
на топики Talent (ADR-0063 п.1).
Compacted-топик одновременно служит источником начальной загрузки: новый потребитель получает полный
справочник чтением топика с offset 0.
Metadata
| Поле | Значение |
|---|---|
| Event type | country-upserted |
| Topic | pin.handbook.countries.v1 — compacted (cleanup.policy = compact); последняя запись каждой страны хранится бессрочно |
| Partitions / key | 1 (десятки строк, строгий порядок) / key = Id записи |
| ContractVersion | 1 |
| Producer | handbook: применённый синк (talent-handbook-sync), admin archive/restore (archive-restore-entry), полная пересинхронизация (talent-initial-import) |
| Статус | draft |
Событие публикуется атомарно с изменением реплики (реплика + чекпоинт синка + публикация — одно согласованное изменение, правила talent-replica.md); после фиксации — доставляется гарантированно (at-least-once).
Payload CountryUpsertedEvent
Полный snapshot реплики страны на момент публикации. Служебные штампы CreatedAt/UpdatedAt в payload
не входят — потребитель ведёт собственные штампы своей реплики.
| Поле | Тип | Обязательность | Источник (поле реплики) | Compatibility | Описание |
|---|---|---|---|---|---|
Id | short | Да | CountryEntity.Id | Immutable | Идентификатор страны = id источника (= Kafka key). |
ShortName | string? | Нет | CountryEntity.ShortName | Additive only | Краткое название («Россия»). |
FullName | string | Да | CountryEntity.FullName | Additive only | Полное название («Российская Федерация»), ≤ 64. Display-имя: Name = FullName — конвенция snapshots.md, отдельного поля Name payload не несёт. |
IsoCode2 | string | Да | CountryEntity.IsoCode2 | Additive only | Код ISO 3166-1 alpha-2 (RU), upper. |
IsoCode3 | string | Да | CountryEntity.IsoCode3 | Additive only | Код ISO 3166-1 alpha-3 (RUS), upper. |
DefaultCurrencyId | short? | Нет | CountryEntity.DefaultCurrencyId | Additive only | Валюта страны по умолчанию — ссылка на запись pin.handbook.currencies.v1; может быть ещё не известна потребителю (порядок топиков разных справочников не гарантирован). |
DefaultLanguageId | short? | Нет | CountryEntity.DefaultLanguageId | Additive only | Язык страны по умолчанию — ссылка на запись pin.handbook.languages.v1. |
IsArchived | bool | Да | CountryEntity.IsArchived | Additive only | Soft-архив: true — запись выведена из оборота (исчезает из lookup-выдач потребителя, исторические ссылки остаются валидными). Tombstone-записей в топике нет — архив передаётся значением. |
ReplicaVersion | long | Да | CountryEntity.ReplicaVersion | Immutable semantics | Версия записи источника; монотонный guard у потребителей — событие с версией ≤ уже применённой пропускается (INV-HND-4). |
Когда публикуется
| Условие | Изменение реплики | Публикация |
|---|---|---|
Применённый upsert синка Talent (handbooks.after-country-information-update), включая auto-restore заархивированной записи | создание / перезапись реплика-полей / IsArchived = false | атомарно с изменением реплики |
| Admin archive / restore | IsArchived переключён | та же |
Полная пересинхронизация (Talent POST sync-handbooks re-эмитит все строки; см. talent-initial-import) | re-emit всех записей справочника (в том числе без фактических изменений) | та же |
Потребители (consumers)
| Consumer-контекст | Эффект | Идемпотентность | Задержка (SLA) |
|---|---|---|---|
chessboard (группа pin-chessboard-handbook-sync, handbook-sync) | Локальная реплика chessboard.handbook_*: валидация CountryId на write-path проектов (default — Россия, CountryId = 1), гео-фильтры и карточки | guard по ReplicaVersion (стейл — skip); повтор доставки — идемпотентный no-op | < 1 мин |
Порядок, идемпотентность и bootstrap
- Порядок per-key. Key =
Id— все события одной страны строго упорядочены; потребитель держит guard поReplicaVersion(событие с версией ≤ применённой — skip + метрика stale). - Полный snapshot — замещение, не слияние. Потребитель перезаписывает свою реплику целиком; отсутствие optional-поля означает «значение очищено», а не «оставить прежнее».
- Compaction. Промежуточные версии записи могут быть вычищены компакцией — потребитель обязан
корректно обрабатывать «скачки»
ReplicaVersion(каждое событие самодостаточно). Физических удалений и tombstone-ов нет: запись выводится из оборота значениемIsArchived = true. - Bootstrap. Новый потребитель получает полный актуальный справочник чтением топика с offset 0; re-emit при пересинхронизации для уже подписанных — идемпотентный no-op (версия не выросла).
Доставка и повторная обработка (retry/DLQ)
| Аспект | Правило |
|---|---|
| Гарантия доставки | at-least-once; публикация атомарна с изменением реплики — «событие без изменения» и «изменение без события» исключены |
| Повтор у потребителя | повторная доставка того же события — идемпотентный no-op (version-guard) |
| Transient-ошибка обработки | retry с экспоненциальным backoff, offset не коммитится, поток топика ждёт |
| Неустранимая ошибка обработки | событие — в собственный DLQ потребителя + алерт; правила — в consumer-доке потребителя (handbook-sync) |
| Отказ применения на стороне producer | событие не публикуется: неприменимое событие источника ушло в pin.handbook.talent-sync-dlq.v1 (см. talent-handbook-sync), реплика и топик не изменились |
Версионирование и совместимость
- Версия — один
ContractVersion = 1; consumer при несоответствии версии события логирует warning и продолжает обработку. Топик кодирует версию суффиксом.v1. - Additive-only: новые поля добавляются как optional с безопасным default — без bump; неизвестные поля потребитель игнорирует.
- Breaking (удаление/переименование/смена типа или смысла поля) — новый
ContractVersion = 2+ новый топикpin.handbook.countries.v2+ dual-publish на период миграции + deadline миграции consumers + sunset.v1. - Новый потребитель подключается в любой момент без участия producer — bootstrap чтением compacted-топика с offset 0.
Безопасность
| Поле | Класс данных | Правило |
|---|---|---|
| все поля payload | Public (нормативный справочник) | include |
PII, secrets, tokens в событии отсутствуют — данные публичные справочные.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (3): Реплика справочников handbook в chessboard (chessboard.handbook_*), Реплика Talent Handbook: правила синка + Entity HandbookSyncState, Entity: CountryEntity
- Use Cases (1): UC-HND-002. Система синхронизирует реплику справочника из Talent
- События (4): Event Contract: city-upserted (CityUpsertedEvent), Event Contract: currency-upserted (CurrencyUpsertedEvent), Event Contract: language-upserted (LanguageUpsertedEvent), Event Contract: region-upserted (RegionUpsertedEvent)
- Консюмеры (2): Consumer: Handbook Sync (pin-chessboard-handbook-sync), Consumer: Talent Handbook Sync (pin-handbook-talent-sync)
- index.md (1): Handbook Service
- registries (1): Реестр событий PIN
Связанные документы
- Сущность: country.md (дом полей, INV-HND-1/2/4/5);
snapshot-модель
CountrySnapshot: snapshots.md. - Producer: talent-handbook-sync, archive-restore-entry, talent-initial-import.
- Потребитель: chessboard/consumers/handbook-sync.md.
- Соседние контракты справочника: region-upserted.md, city-upserted.md, currency-upserted.md, language-upserted.md.
- ADR-0063.