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