Consumer: Handbook Sync (pin-chessboard-handbook-sync)
Консолидированный документ Kafka-консюмера локальной реплики справочников в контексте chessboard. Один
consumer group обрабатывает четыре compacted-топика pin.handbook.*.v1 (страны, регионы, города, валюты);
топик языков не потребляется — у chessboard нет ссылок на языки. Поля реплики и правила её ведения
HR-1..HR-7 — дом handbook-replica.md; контракты входных событий — дома
handbook (country-upserted,
region-upserted,
city-upserted,
currency-upserted) — здесь не переописываются.
Retry/DLQ/replay-политика группы описана здесь единожды на все потоки.
Назначение
Consumer group pin-chessboard-handbook-sync поддерживает в chessboard локальную реплику справочников
chessboard.handbook_* — единственную локальную реплику справочников в PIN
(ADR-0063 п.6): валидация ссылок
CountryId/RegionId/CityId/CurrencyId на write-path проектов и цен выполняется без сетевого вызова
в handbook. Вторая обязанность — refresh денормализованных имён RegionName/CityName проектов при
переименованиях в справочнике с переопубликацией project-upserted затронутых проектов (HR-6), чтобы
проекции catalog догнали новое имя. На топики Talent chessboard не подписывается (ADR-0063 п.1).
Owning-сервис и триггер
| Поле | Значение |
|---|---|
| Сервис/контекст | chessboard |
| Consumer group | pin-chessboard-handbook-sync |
| Producer | контекст handbook (talent-handbook-sync, admin archive/restore, пересинхронизация) |
| Топик 1 | pin.handbook.countries.v1 — полный snapshot страны |
| Топик 2 | pin.handbook.regions.v1 — полный snapshot региона |
| Топик 3 | pin.handbook.cities.v1 — полный snapshot города (вложенные снапшоты Country/Region не сохраняются) |
| Топик 4 | pin.handbook.currencies.v1 — полный snapshot валюты |
| ContractVersion | ожидается 1 для всех событий; при несовпадении — warn-and-proceed + метрика |
| Порядок | key = Id записи → строгий порядок per-record; порядок между топиками не гарантирован — компенсируется логическими ссылками без FK (HR-4) |
| DLQ | pin.chessboard.handbook-sync-dlq.v1 |
Маппинг «событие → эффект» (бизнес-термины):
| Поток | Событие (топик) | Эффект |
|---|---|---|
| A | country-upserted | Upsert HandbookCountryReplica |
| B | region-upserted | Upsert HandbookRegionReplica; при смене Name — refresh RegionName проектов + re-publish project-upserted (HR-6) |
| C | city-upserted | Upsert HandbookCityReplica; при смене Name — refresh CityName проектов + re-publish project-upserted (HR-6) |
| D | currency-upserted | Upsert HandbookCurrencyReplica |
| — | language-upserted (pin.handbook.languages.v1) | Не потребляется — ссылок на языки в chessboard нет |
Контекст события
- Из метаданных события доступны:
MessageId(идентификатор для дедупликации),OccurredAt, идентификатор записи справочника (key). Актор — система синка (не пользовательский контекст); реплика платформенная, tenant-фильтр не применяется, PII нет. - Ключ идемпотентности:
chessboard-handbook-sync:{eventType}:{MessageId},eventType ∈ {country, region, city, currency}; бизнес-идемпотентность — version-guard HR-2 (payload.ReplicaVersion >хранимой версии; регион —payload.LastEventAt >хранимогоLastEventAt). - Корреляция сквозного трейса восстанавливается из метаданных события; при refresh HR-6 пробрасывается
в переопубликованные
project-upserted.
Входные данные
Payload каждого события — полный snapshot записи (контракты — дома handbook, ссылки выше). Chessboard
читает только поля, хранимые репликой (таблицы полей — handbook-replica.md):
ShortName/DefaultCurrencyId/DefaultLanguageId страны; вложенные снапшоты Country/Region внутри
payload city-события игнорируются — актуальные имена родительской географии ведутся собственными строками
реплики по их топикам.
Алгоритм
Все четыре потока симметричны; дельты — time-based guard региона (HR-2) и refresh имён (HR-6, только регионы/города).
- Контекст события. Метаданные (
MessageId, key); сверкаContractVersion = 1— при несовпадении warn-and-proceed + метрика; идемпотентность по ключу — дубль → логskipped-duplicate, ack. - Version-guard (HR-2). Запись есть и версия payload не больше хранимой (
ReplicaVersion; регион —LastEventAt) → skip + метрика stale, ack. Компакция топика и пересинхронизацияhandbookдают «скачки» версии — каждое событие самодостаточно. - Upsert (HR-3, HR-4, HR-5). Создание/перезапись всех полей записи реплики полным snapshot-ом
(отсутствие optional-поля — «значение очищено»); ссылки
CountryId/RegionId— логические, без проверки существования (нерезолвимость допустима, полнота гео-цепочки проверяется на write-path проектов);IsArchivedприменяется значением payload — физических удалений нет;SyncedAt = now. - Refresh имён (HR-6, потоки B/C). Если применённый upsert изменил
Nameотносительно хранимого: у всех проектов с этимRegionId/CityId, чьёRegionName/CityNameотличается от нового имени, — поле обновляется, версия проекта растёт, для каждого затронутого проекта переопубликуетсяproject-upserted(полный snapshot). Условие «отличается» делает refresh идемпотентным. Archive/restore без смены имени и события стран/валют refresh не триггерят. - Согласованность. Запись реплики + отметка идемпотентности + refresh затронутых проектов + их события — одно согласованное изменение; offset подтверждается только после фиксации.
- Результат. Успех → ack; transient-сбой → retry по политике (таблица ниже); неустранимый дефект payload → DLQ + алерт.
Диаграмма (поток C — city с refresh имени)
Идемпотентность и replay
- По событию: дедупликация по
MessageId— повторная доставка after-commit-crash не меняет состояние. - По бизнес-правилу: version-guard HR-2 — устаревшее событие пропускается; refresh HR-6 идемпотентен (обновляются только проекты с отличающимся именем).
- Replay: да, безопасен. Bootstrap и восстановление — чтение всех четырёх compacted-топиков с
offset 0 (HR-7); re-emit при пересинхронизации
handbook— идемпотентный no-op, кроме записей с фактическими изменениями. Точечный replay DLQ — после разбора дефекта с командойhandbook.
Retry и DLQ
DLQ-топик группы: pin.chessboard.handbook-sync-dlq.v1 (исходное событие + заголовки x-dlq-reason,
x-dlq-error-code, x-source-topic, x-original-offset).
| Сбой | Retry | DLQ | Ручное действие |
|---|---|---|---|
| Инфраструктурный (PostgreSQL/шина недоступны, timeout) | Бесконечный exponential backoff 1s→60s с паузой партиции (событие не теряется) | Нет | Восстановить зависимость |
| Гонка refresh HR-6 с конкурентной правкой проекта (оптимистическая блокировка) | 3 попытки немедленно с перечитыванием проекта, далее backoff | После исчерпания + алерт | Расследование гонки; replay из DLQ |
Дефект payload (контрактное нарушение: пустое Name, неположительный Id) | Нет (non-retriable) | Да + алерт | Разбор с командой handbook (дефект producer-а) → replay из DLQ |
Несовпадение ContractVersion | — (warn-and-proceed, не сбой) | Нет | Сигнал смены контракта — согласовать миграцию на .v2 |
Гео-согласованность на консюмере не проверяется (HR-4): рассогласование цепочки — забота write-path
проектов (CHESSBOARD_CITY_REGION_MISMATCH и др. — handbook-replica.md),
а дефекты данных источника отсекаются синком handbook до публикации в pin.handbook.*.
Наблюдаемость и безопасность
- Logs (structured, по событию):
consumed/skipped-duplicate/skipped-stale/applied/name-refreshed(с числом затронутых проектов) /dead-letteredс полямиMessageId, тип записи,Id, версия payload. PII нет. - Metrics:
chessboard_handbook_sync_lag_seconds(по типу записи),chessboard_handbook_sync_stale_events_total,chessboard_handbook_sync_dlq_total,chessboard_handbook_sync_contract_mismatch_total,chessboard_handbook_name_refresh_projects_total. - Traces: consume-span → span применения (+ span refresh HR-6); корреляция пробрасывается в
переопубликованные
project-upserted. - Alerts: DLQ > 0; лаг применения > 5 мин при ненулевом потоке (SLA применения < 1 мин);
contract_mismatch> 0. - Безопасность: consumer работает под system-actor; реплика не tenanted; изменения проектов при refresh штампуются system-actor-ом консюмера.
Пред/постусловия
- До обработки события. Реплика может не содержать целевую запись (upsert); родительские записи географии могут ещё не приехать (HR-4 — не блокирует). Порядок per-record гарантирован ключом топика.
- После обработки применённого события. Запись реплики отражает последний snapshot
handbook(ReplicaVersion/LastEventAt,IsArchived,SyncedAtобновлены); при смене имени региона/города — денормализованные имена всех затронутых проектов обновлены и по каждому переопубликованproject-upserted; offset ack — только после фиксации. Устаревшее/дублирующее событие — состояние не изменено, ack.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Реплика справочников handbook в chessboard (chessboard.handbook_*), Entity / Aggregate: ProjectEntity
- API (1): PUT /api/v1/developers/projects/{projectId} — Обновление проекта
- События (6): Event Contract: city-upserted (CityUpsertedEvent), Event Contract: country-upserted (CountryUpsertedEvent), Event Contract: currency-upserted (CurrencyUpsertedEvent), Event Contract: language-upserted (LanguageUpsertedEvent), Event Contract: pin.chessboard.project-upserted (ProjectUpsertedKafkaEvent), Event Contract: region-upserted (RegionUpsertedEvent)
- Консюмеры (1): Consumer: Talent Chessboard Sync (pin-chessboard-talent-sync)
- index.md (1): Chessboard Service (Pin.Chessboard)
Связанные документы
- Поля реплики и правила HR-1..HR-7, write-path-валидация, fallback-резолв: handbook-replica.md.
- Контракты входных событий: country-upserted, region-upserted, city-upserted, currency-upserted.
- Переопубликуемое событие: project-upserted; целевые поля refresh — project.md.
- Producer-сторона: handbook/consumers/talent-handbook-sync.md;
bootstrap
handbook— talent-initial-import.md. - ADR-0063 п.1/п.6; ADR-0056 (simplicity-first).