Background Job: talent-initial-import (bootstrap и reconciliation справочников из Talent Handbook)
Процедура начальной загрузки и полной пересинхронизации реплики справочников контекста handbook. В
отличие от identity-bootstrap, PIN сам страницы источника не читает: Talent Handbook по команде
POST sync-handbooks re-эмитит все строки справочников в свои обычные топики handbooks.after-*, а
штатный consumer talent-handbook-sync применяет их обычными
правилами R1–R9 (talent-replica.md). Job на стороне PIN фиксирует окно
ресинка, отслеживает прогресс и строит reconciliation-отчёт по «зомби-записям» (delete-gap источника,
ADR-0063 п.5).
Назначение
Два режима одной процедуры:
- Bootstrap — первичное наполнение пустой реплики при заведении окружения: инициировать re-эмиссию всех строк источника и дождаться их применения consumer-ом; после завершения актуализация — только потоковыми событиями.
- Reconciliation — регламентная сверка наполненной реплики с источником: зафиксировать окно ресинка, инициировать re-эмиссию, по закрытии окна построить отчёт «строки реплики, не пришедшие в окно» — кандидаты в архив (источник их больше не ведёт, а delete-событий не шлёт). Авто-архива нет: каждый кандидат подтверждает администратор платформы (защита от ложных срабатываний при частичном ресинке).
Downstream-потребителям (chessboard и будущим) этот job не нужен: их bootstrap — чтение compacted
pin.handbook.*.v1 с offset 0 (по одному последнему snapshot на запись).
Классификация
| Поле | Значение |
|---|---|
| Сервис | handbook |
| Область действия | Реплики handbook.countries / regions / cities / currencies / languages; чекпоинты handbook.talent_sync_states; оперативные handbook.resync_runs / handbook.resync_candidates |
| Тип job | One-off / по требованию (bootstrap — разово; reconciliation — регламентно, рекомендация — раз в квартал и при подозрении на delete-gap) |
| Trigger | Ручной запуск администратором платформы; re-эмиссию у источника инициирует оператор Talent Handbook (POST sync-handbooks — операция источника, вызов по согласованию с командой Talent) |
| Критичность | medium (без bootstrap нет справочных данных — но запускается контролируемо, вне горячего пути; reconciliation не блокирует работу платформы) |
Расписание и защита от повторного запуска
| Аспект | Правило |
|---|---|
| Schedule | Нет (по требованию); регламентная рекомендация для reconciliation — квартально |
| Единственный запуск | Одна активная пересинхронизация на кластер: блокировка Pin:lock:job:handbook-talent-resync (PostgreSQL advisory lock, без Redis — ADR-0056); запуск при активном окне → отказ (без ретрая) |
| Окно ресинка | WindowStartedAt фиксируется при старте; закрытие — по критерию «поток иссяк» (за QuietPeriod, default 10 мин, consumer не применил и не skip-нул ни одного события источника) либо по WindowTimeout (default 120 мин) либо вручную оператором |
| Max duration | Полный ресинк (envelope: десятки стран/валют/языков, сотни регионов, тысячи городов) — минуты потока + QuietPeriod; окно целиком < 2 ч |
Входная модель HandbookResyncRequest
| Поле | Тип | Обязательность | Источник | Описание |
|---|---|---|---|---|
Mode | HandbookResyncMode | Да | вход администратора | Bootstrap / Reconciliation (enum, хранится text) |
AggregateTypes | HandbookAggregateType[] | Нет (default — все 5) | вход администратора | Ограничить сверку подмножеством справочников (Country/Region/City/Currency/Language); частичный scope сужает и reconciliation-отчёт |
QuietPeriod | duration | Нет (default 10 мин) | вход администратора | Тишина потока, по которой окно считается закрытым |
WindowTimeout | duration | Нет (default 120 мин) | вход администратора | Максимальная длительность окна; по истечении окно закрывается принудительно + warn |
CorrelationId | Guid | Да | генерируется | Сквозной trace запуска и отчёта |
Tenant не применяется — справочники платформенные; job работает под system-actor.
Оперативное состояние (job-owned)
| Сущность | Ключевые поля | Назначение |
|---|---|---|
HandbookResyncRunEntity (handbook.resync_runs) | Id, Mode, Status (Running / AwaitingReview / Completed / Failed / Cancelled), WindowStartedAt, WindowClosedAt?, AggregateTypes, счётчики по типам (применено / stale-skip / DLQ за окно), CandidatesTotal, CorrelationId, штампы | Один запуск; защита от двойного старта вместе с блокировкой; AwaitingReview — только Reconciliation (отчёт построен, ждёт решений администратора) |
HandbookResyncCandidateEntity (handbook.resync_candidates) | Id, RunId, AggregateType, ExternalId, Name (display на момент отчёта), LastEventAt?, Resolution (Pending / Archived / Kept), ResolvedByUserId?, ResolvedAt? | Строка reconciliation-отчёта: запись реплики, не пришедшая в окно; решение фиксируется поимённо с актором |
Алгоритм
- Контекст. Актор — администратор платформы (инициатор) + system-actor исполнения; захватить
блокировку запуска (занята → отказ, без ретрая); создать
HandbookResyncRunEntity(Status = Running,WindowStartedAt = now); проверить готовность: consumer группыpin-handbook-talent-syncработает, DLQ разобран (непустой DLQ → warn в отчёт запуска — конфликтные записи в окно не доедут). - Инициация re-эмиссии. Оператор Talent Handbook вызывает у источника
POST sync-handbooks— источник re-эмитит все строки справочников в обычные топикиhandbooks.after-*. PIN операцию источника не вызывает сам (внешняя система, доступ — у команды Talent); факт инициации фиксируется в запуске. - Штатное применение. Consumer применяет поток обычными правилами — job в применение не вмешивается:
новые/изменённые строки → upsert + re-publish
pin.handbook.*; неизменённые → stale-skip с обновлением отметки живостиHandbookSyncState.LastEventId/LastEventAt(R2); заархивированные в PIN, но живые в источнике → auto-restore + warn-метрика (R5); конфликты → DLQ (R6, INV-HND-3). - Мониторинг окна. Job периодически опрашивает прогресс (счётчики применённых/stale за окно по
HandbookSyncState.LastEventAt >= WindowStartedAt); окно закрывается при тишине потокаQuietPeriod, поWindowTimeout(принудительно + warn — ресинк мог не завершиться) либо вручную;WindowClosedAt = now, счётчики фиксируются в запуске. - Reconciliation-отчёт (
Mode = Reconciliation). Кандидаты в архив: неархивные записи реплики типов изAggregateTypes, по которымHandbookSyncState.LastEventAt < WindowStartedAt(или чекпоинта нет) — источник их в окно не re-эмитил. Для каждого — строкаHandbookResyncCandidateEntity(Resolution = Pending); регион считается пришедшим, если в окно пришло хотя бы одно city-событие с егоRegionEventModel.Run.Status = AwaitingReview. Санити-порог: кандидатов > 10 % строк справочника → алерт «подозрение на частичный ресинк», рекомендация не архивировать без сверки с источником. ПриMode = Bootstrapшаг пропускается (сравнивать не с чем) —Run.Status = Completed. - Решения администратора. Авто-архива нет. По каждому кандидату администратор платформы: либо
архивирует запись существующей admin-операцией
(archive-restore-entry.md —
IsArchived = true+ re-publish события с признаком архива; INV-HND-5: запись уходит из lookup, остаётся резолвимой) —Resolution = Archived; либо оставляет (Resolution = Kept, например строка не пришла из-за частичного ресинка). Ошибочный архив обратим restore-операцией (guard INV-HND-2 на ISO-коды). - Завершение. Все кандидаты разрешены (
Pending= 0) →Run.Status = Completed; блокировка освобождена; метрики записаны. Прерывание оператором →Cancelled(принятые решения сохраняются).
Согласованность изменения
| Элемент | Правило |
|---|---|
| Граница | Job не мутирует реплику: применение — только штатный consumer (R8), архив — только admin-операция; job пишет лишь собственные Run/Candidates |
| Сохраняемое состояние | HandbookResyncRunEntity (статусы, окно, счётчики) + HandbookResyncCandidateEntity (решения с актором) |
| Внешние вызовы | POST sync-handbooks — операция источника, инициируется оператором Talent вне транзакций PIN |
| Частичная ошибка | Сбой мониторинга/построения отчёта не влияет на реплику (consumer независим); перезапуск шага отчёта детерминирован (тот же WindowStartedAt → тот же список кандидатов) |
Идемпотентность и retry
| Сценарий | Правило |
|---|---|
| Повторный запуск ресинка | Безопасен: re-эмиссия → upsert + version-guard → stale-skip для неизменённого (реплика не мутируется), отметки живости обновляются; новый Run открывает новое окно |
| Рестарт job | Resume: активный Run(Running/AwaitingReview) подхватывается по блокировке/статусу; окно и кандидаты сохранены |
| Дубль кандидата | Одна строка на (RunId, AggregateType, ExternalId); повторное построение отчёта того же окна — upsert без дублей |
| Poison-события в окне | Уходят в DLQ штатной политикой consumer-а (R6, INV-HND-3); job фиксирует счётчик DLQ за окно в запуске — такие записи требуют разбора до архивных решений |
| Гонка окна и решений | Кандидат, по которому после закрытия окна пришло событие источника (запоздавшая re-эмиссия), при попытке архива подсвечивается: HandbookSyncState.LastEventAt >= WindowStartedAt → рекомендация Kept |
| Откат ошибочного архива | Restore admin-операцией: IsArchived = false + re-publish события — потребители восстанавливают запись автоматически |
Runbook полной пересинхронизации
- Подготовка. Убедиться: consumer группы
pin-handbook-talent-syncработает, лаг в норме, DLQ пуст (иначе — разобрать: правка данных в источнике → replay). Согласовать с командой Talent вызовPOST sync-handbooks. - Старт окна. Запустить job (
Mode = Reconciliation; для пустой реплики —Bootstrap); зафиксированWindowStartedAt. - Re-эмиссия. Оператор Talent вызывает
POST sync-handbooks; наблюдать поток: растутhandbook_sync_stale_events_total(неизменённые строки) и счётчики применённого; следить, чтоhandbook_sync_dlq_totalне растёт. - Закрытие окна. Дождаться авто-закрытия (тишина
QuietPeriod) либо закрыть вручную; закрытие поWindowTimeout— сигнал незавершённой re-эмиссии: согласовать повтор с Talent, кандидатов не архивировать. - Разбор отчёта (
Reconciliation). Просмотреть кандидатов; массовые кандидаты (> 10 % справочника) — признак частичного ресинка, не архивировать; точечные — сверить с источником и архивировать admin-операцией (каждый архив re-publish-ится, downstream догоняет автоматически). - Критерии успеха. Новых событий в DLQ за окно — 0 (либо все разобраны); счётчики применено+stale по
каждому типу соответствуют объёму справочника источника;
Pending-кандидатов — 0; лаг downstream-потребителейpin.handbook.*(chessboard) — в норме; устаревшие имена регионов закрыты (ограничение переименований region.md гарантированно снимается полным ресинком). - Откат. Пересинхронизация сама по себе реплику не портит (идемпотентна); откатывать нечего, кроме
ошибочных архивных решений — restore-операция по каждому (п. «Идемпотентность»). Прерывание окна —
Cancelled, повторный запуск с новым окном.
Наблюдаемость
| Тип | Правило |
|---|---|
| Logs | resync-started / window-progress / window-closed / report-built / candidate-resolved / resync-completed/-failed/-cancelled с RunId, Mode, счётчиками по AggregateType, CorrelationId |
| Metrics | handbook_resync_running (gauge 0/1), handbook_resync_window_duration_seconds, handbook_resync_events_applied_total{aggregate}, handbook_resync_candidates_total{aggregate}, handbook_resync_candidates_pending (gauge); переиспользуются метрики consumer-а (handbook_sync_stale_events_total, handbook_sync_dlq_total, handbook_sync_auto_restores_total) |
| Traces | CorrelationId запуска → span окна → span отчёта; события consumer-а несут собственную корреляцию |
| Alerts | окно закрыто по WindowTimeout (незавершённая re-эмиссия); кандидатов > 10 % строк справочника (частичный ресинк); рост DLQ за окно > 0; candidates_pending > 0 дольше 7 дней (забытый отчёт) |
| Dashboard | handbook-sync-overview: прогресс окна, счётчики по типам, кандидаты и решения |
Пред/постусловия
- До запуска. Consumer работает; блокировка запусков свободна; для
Reconciliation— реплика наполнена и DLQ разобран; согласована re-эмиссия с командой Talent. - После
Completed(Bootstrap). Каждая строка источника представлена репликой (ReplicaVersion/LastEventAtзаполнены); по всем применённым записям ушли событияpin.handbook.*; downstream может бутстрапиться с offset 0 compacted-топиков. - После
Completed(Reconciliation). Живые строки источника подтверждены отметками живости (LastEventAt >= WindowStartedAt); все кандидаты разрешены поимённо (Archived— через admin-операцию с re-publish,Kept— с фиксацией актора); авто-архивов не было; исторические ссылки на архивные записи резолвимы (INV-HND-5).
Тесты
| Тест | Ожидание |
|---|---|
| Bootstrap пустой реплики | После re-эмиссии реплика соответствует источнику; по каждой записи ушло *-upserted; Run = Completed |
| Повторный ресинк без изменений | Все события — stale-skip; реплика не изменена; отметки живости обновлены; кандидатов — 0 |
| Delete-gap | Строка удалена в источнике → не re-эмитится → попадает в кандидаты; авто-архива нет; после подтверждения администратором — IsArchived = true + re-publish |
| Частичный ресинк | Окно закрыто по WindowTimeout → warn; массовые кандидаты → алерт; решения не навязываются |
| Запоздавшая re-эмиссия | Событие по кандидату после закрытия окна → кандидат подсвечен (LastEventAt >= WindowStartedAt), рекомендация Kept |
| Ошибочный архив | Restore-операция возвращает запись в lookup + re-publish; downstream восстанавливается без ручных действий |
| Двойной запуск | Второй запуск при активном окне → отказ (блокировка) |
| Прерывание | Cancelled; принятые решения сохранены; повторный запуск открывает новое окно |
Известные ограничения и assumptions
POST sync-handbooks— операция источника; PIN её не вызывает и не контролирует полноту re-эмиссии (частичная re-эмиссия → ложноположительные кандидаты — поэтому авто-архив запрещён).- Имена топиков и операции источника (
handbooks.after-*,POST sync-handbooks) — assumption до подтверждения schema registry / интеграционного контракта Talent (ADR-0063). - Re-эмиссия идёт в общие топики: на время окна растёт нагрузка на consumer и всплеск stale-метрик — допустимо (объёмы — тысячи строк, ADR-0056).
- Устаревание имён регионов между ресинками (нет region-событий у источника) — принятое ограничение ADR-0063 п.4; закрывается этим job-ом.
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (2): Реплика Talent Handbook: правила синка + Entity HandbookSyncState, Entity: RegionEntity
- API (3): GET /api/v1/admin/handbook/sync-state — Состояние синхронизации справочников с Talent, Handbook API — реестр операций областей public / internal / admin, POST /api/v1/admin/handbook/{type}/{id}/{archive,restore} — Архив и восстановление справочной записи
- Use Cases (3): UC-HND-001. Администратор поддерживает справочник (archive/restore), UC-HND-002. Система синхронизирует реплику справочника из Talent, US-реестр контекста handbook (US-HND)
- События (5): Event Contract: city-upserted (CityUpsertedEvent), Event Contract: country-upserted (CountryUpsertedEvent), 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
Связанные документы
- Consumer (штатное применение потока): ../consumers/talent-handbook-sync.md;
правила R1–R9 и
HandbookSyncState: ../domain/talent-replica.md (R9). - Admin archive/restore (исполнение решений по кандидатам): ../api/admin/archive-restore-entry.md; состояние синка: ../api/admin/get-sync-state.md.
- Целевые реплики: country · region · city · currency · language.
- Решения: ADR-0063 п.5 (delete-gap → архив + reconciliation), ADR-0056 (simplicity-first); образец — identity/background/talent-initial-import.md.