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

Event Contract: city-upserted (CityUpsertedEvent)

Версионируемый контракт интеграционного события «запись города справочника создана/изменена». Consumer-документы ссылаются сюда, не переописывая payload. Дом полей — entity: CityEntity.

Назначение

Фиксирует каждое изменение состояния реплики города в контексте handbook — самого объёмного и самого востребованного справочника: города — ключ гео-фильтров каталога, адресов проектов и пожеланий лидов. Помимо полей записи payload несёт вложенные снапшоты родительской географии (CountrySnapshot, RegionSnapshot?), чтобы потребитель получал человекочитаемые имена страны/региона без ожидания событий соседних топиков справочника. Compacted-топик служит источником начальной загрузки (чтение с offset 0).

Metadata

ПолеЗначение
Event typecity-upserted
Topicpin.handbook.cities.v1compacted (cleanup.policy = compact); последняя запись каждого города хранится бессрочно
Partitions / key1 (тысячи строк, редкие изменения, строгий порядок) / key = Id записи
ContractVersion1
Producerhandbook: применённый синк (talent-handbook-sync), admin archive/restore (archive-restore-entry), полная пересинхронизация (talent-initial-import)
Статусdraft
Атомарность и доставка

Событие публикуется атомарно с изменением реплики (реплика + чекпоинт синка + публикация — одно согласованное изменение, правила talent-replica.md); после фиксации — доставляется гарантированно (at-least-once). Порядок применения geo-справочников синком (страна → регион → город) гарантирует: к моменту публикации city-события страна и регион города уже существуют в реплике handbook — снапшоты строятся всегда.

Payload CityUpsertedEvent

Полный snapshot реплики города на момент публикации + вложенные снапшоты родительской географии. Служебные штампы CreatedAt/UpdatedAt в payload не входят.

ПолеТипОбязательностьИсточник (поле реплики)CompatibilityОписание
IdlongДаCityEntity.IdImmutableИдентификатор города = id источника (= Kafka key).
CountryIdshortДаCityEntity.CountryIdAdditive onlyСтрана города.
RegionIdlong?НетCityEntity.RegionIdAdditive onlyРегион города; null — город вне региона (напр., города федерального значения).
NamestringДаCityEntity.NameAdditive onlyНазвание города («Самара»), ≤ 256.
CountryCountrySnapshotДареплика страны на момент публикацииAdditive onlyСнапшот страны — поля см. snapshots.md, здесь не переописываются.
RegionRegionSnapshot?Нет (задан ⇔ RegionId ≠ null)реплика региона на момент публикацииAdditive onlyСнапшот региона — поля см. snapshots.md.
IsArchivedboolДаCityEntity.IsArchivedAdditive onlySoft-архив: true — запись выведена из оборота. Tombstone-записей в топике нет — архив передаётся значением.
ReplicaVersionlongДаCityEntity.ReplicaVersionImmutable semanticsВерсия записи источника; монотонный guard у потребителей (INV-HND-4).

Снапшоты Country/Region зафиксированы на момент публикации и не «догоняются» (правило 3 snapshots.md): последующее переименование страны/региона уже опубликованные payload-ы не меняет — актуальные имена потребитель ведёт по событиям country-upserted / region-upserted.

Когда публикуется

УсловиеИзменение репликиПубликация
Применённый upsert синка Talent (handbooks.after-city-information-update), включая auto-restore заархивированной записисоздание / перезапись реплика-полей / IsArchived = falseатомарно с изменением реплики
Admin archive / restoreIsArchived переключёнта же
Полная пересинхронизация (Talent POST sync-handbooks re-эмитит все строки; см. talent-initial-import)re-emit всех записей справочниката же
Порядок публикаций

Одно city-событие источника может породить две публикации handbook: сначала region-upserted (если derive изменил регион), затем city-upserted — в этом порядке.

Потребители (consumers)

Consumer-контекстЭффектИдемпотентностьЗадержка (SLA)
chessboard (группа pin-chessboard-handbook-sync, handbook-sync)Локальная реплика chessboard.handbook_*: валидация CityId на write-path проектов, гео-фильтры и карточки; при переименовании города — refresh денормализованных CityName затронутых проектов + re-publish project-upserted (чтобы проекции catalog догнали)guard по ReplicaVersion (стейл — skip); повтор доставки — идемпотентный no-op< 1 мин

Порядок, идемпотентность и bootstrap

  • Порядок per-key. Key = Id — все события одного города строго упорядочены; потребитель держит guard по ReplicaVersion (событие с версией ≤ применённой — skip + метрика stale).
  • Полный snapshot — замещение, не слияние. Потребитель перезаписывает свою реплику целиком; отсутствие optional-поля (RegionId/Region) означает «город вне региона», а не «оставить прежнее значение».
  • Compaction. Промежуточные версии записи могут быть вычищены компакцией — каждое событие самодостаточно. Физических удалений и tombstone-ов нет: вывод из оборота — значением IsArchived = true.
  • Bootstrap. Новый потребитель получает полный актуальный справочник чтением топика с offset 0; благодаря вложенным снапшотам display-имена страны/региона доступны сразу, не дожидаясь чтения соседних топиков.
  • Порядок между топиками справочника не гарантирован: ссылки CountryId/RegionId до прихода соответствующих событий могут быть нерезолвимы в реплике потребителя — display-поля на этот период берутся из вложенных снапшотов.

Доставка и повторная обработка (retry/DLQ)

АспектПравило
Гарантия доставкиat-least-once; публикация атомарна с изменением реплики
Повтор у потребителяповторная доставка того же события — идемпотентный no-op (version-guard)
Transient-ошибка обработкиretry с экспоненциальным backoff, offset не коммитится
Неустранимая ошибка обработкисобытие — в собственный DLQ потребителя + алерт; правила — в consumer-доке потребителя (handbook-sync)
Отказ применения на стороне producerсобытие не публикуется: неприменимое событие источника (HANDBOOK_SYNC_GEO_MISMATCH — non-retriable, HANDBOOK_SYNC_COUNTRY_NOT_FOUND — после исчерпания ретраев) ушло в pin.handbook.talent-sync-dlq.v1 (см. talent-handbook-sync), реплика и топик не изменились

Версионирование и совместимость

  • Версия — один ContractVersion = 1; consumer при несоответствии версии события логирует warning и продолжает обработку. Топик кодирует версию суффиксом .v1.
  • Additive-only: новые поля (в том числе новые поля вложенных снапшотов — версионируются вместе с несущим контрактом, snapshots.md) добавляются как optional с безопасным default — без bump; неизвестные поля потребитель игнорирует.
  • Breaking — новый ContractVersion = 2 + новый топик pin.handbook.cities.v2 + dual-publish + deadline миграции consumers + sunset .v1.
  • Новый потребитель подключается в любой момент — bootstrap чтением compacted-топика с offset 0.

Безопасность

ПолеКласс данныхПравило
все поля payload (включая вложенные снапшоты)Public (нормативный справочник)include

PII, secrets, tokens в событии отсутствуют — данные публичные справочные.

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

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

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