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

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 не участвует

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
Тип jobOne-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

ПолеТипОбязательностьИсточникОписание
ModeHandbookResyncModeДавход администратораBootstrap / Reconciliation (enum, хранится text)
AggregateTypesHandbookAggregateType[]Нет (default — все 5)вход администратораОграничить сверку подмножеством справочников (Country/Region/City/Currency/Language); частичный scope сужает и reconciliation-отчёт
QuietPerioddurationНет (default 10 мин)вход администратораТишина потока, по которой окно считается закрытым
WindowTimeoutdurationНет (default 120 мин)вход администратораМаксимальная длительность окна; по истечении окно закрывается принудительно + warn
CorrelationIdGuidДагенерируетсяСквозной trace запуска и отчёта
Tenant

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-отчёта: запись реплики, не пришедшая в окно; решение фиксируется поимённо с актором

Алгоритм

  1. Контекст. Актор — администратор платформы (инициатор) + system-actor исполнения; захватить блокировку запуска (занята → отказ, без ретрая); создать HandbookResyncRunEntity (Status = Running, WindowStartedAt = now); проверить готовность: consumer группы pin-handbook-talent-sync работает, DLQ разобран (непустой DLQ → warn в отчёт запуска — конфликтные записи в окно не доедут).
  2. Инициация re-эмиссии. Оператор Talent Handbook вызывает у источника POST sync-handbooks — источник re-эмитит все строки справочников в обычные топики handbooks.after-*. PIN операцию источника не вызывает сам (внешняя система, доступ — у команды Talent); факт инициации фиксируется в запуске.
  3. Штатное применение. Consumer применяет поток обычными правилами — job в применение не вмешивается: новые/изменённые строки → upsert + re-publish pin.handbook.*; неизменённые → stale-skip с обновлением отметки живости HandbookSyncState.LastEventId/LastEventAt (R2); заархивированные в PIN, но живые в источнике → auto-restore + warn-метрика (R5); конфликты → DLQ (R6, INV-HND-3).
  4. Мониторинг окна. Job периодически опрашивает прогресс (счётчики применённых/stale за окно по HandbookSyncState.LastEventAt >= WindowStartedAt); окно закрывается при тишине потока QuietPeriod, по WindowTimeout (принудительно + warn — ресинк мог не завершиться) либо вручную; WindowClosedAt = now, счётчики фиксируются в запуске.
  5. Reconciliation-отчёт (Mode = Reconciliation). Кандидаты в архив: неархивные записи реплики типов из AggregateTypes, по которым HandbookSyncState.LastEventAt < WindowStartedAt (или чекпоинта нет) — источник их в окно не re-эмитил. Для каждого — строка HandbookResyncCandidateEntity (Resolution = Pending); регион считается пришедшим, если в окно пришло хотя бы одно city-событие с его RegionEventModel. Run.Status = AwaitingReview. Санити-порог: кандидатов > 10 % строк справочника → алерт «подозрение на частичный ресинк», рекомендация не архивировать без сверки с источником. При Mode = Bootstrap шаг пропускается (сравнивать не с чем) — Run.Status = Completed.
  6. Решения администратора. Авто-архива нет. По каждому кандидату администратор платформы: либо архивирует запись существующей admin-операцией (archive-restore-entry.mdIsArchived = true + re-publish события с признаком архива; INV-HND-5: запись уходит из lookup, остаётся резолвимой) — Resolution = Archived; либо оставляет (Resolution = Kept, например строка не пришла из-за частичного ресинка). Ошибочный архив обратим restore-операцией (guard INV-HND-2 на ISO-коды).
  7. Завершение. Все кандидаты разрешены (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 открывает новое окно
Рестарт jobResume: активный 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 полной пересинхронизации

  1. Подготовка. Убедиться: consumer группы pin-handbook-talent-sync работает, лаг в норме, DLQ пуст (иначе — разобрать: правка данных в источнике → replay). Согласовать с командой Talent вызов POST sync-handbooks.
  2. Старт окна. Запустить job (Mode = Reconciliation; для пустой реплики — Bootstrap); зафиксирован WindowStartedAt.
  3. Re-эмиссия. Оператор Talent вызывает POST sync-handbooks; наблюдать поток: растут handbook_sync_stale_events_total (неизменённые строки) и счётчики применённого; следить, что handbook_sync_dlq_total не растёт.
  4. Закрытие окна. Дождаться авто-закрытия (тишина QuietPeriod) либо закрыть вручную; закрытие по WindowTimeout — сигнал незавершённой re-эмиссии: согласовать повтор с Talent, кандидатов не архивировать.
  5. Разбор отчёта (Reconciliation). Просмотреть кандидатов; массовые кандидаты (> 10 % справочника) — признак частичного ресинка, не архивировать; точечные — сверить с источником и архивировать admin-операцией (каждый архив re-publish-ится, downstream догоняет автоматически).
  6. Критерии успеха. Новых событий в DLQ за окно — 0 (либо все разобраны); счётчики применено+stale по каждому типу соответствуют объёму справочника источника; Pending-кандидатов — 0; лаг downstream-потребителей pin.handbook.* (chessboard) — в норме; устаревшие имена регионов закрыты (ограничение переименований region.md гарантированно снимается полным ресинком).
  7. Откат. Пересинхронизация сама по себе реплику не портит (идемпотентна); откатывать нечего, кроме ошибочных архивных решений — restore-операция по каждому (п. «Идемпотентность»). Прерывание окна — Cancelled, повторный запуск с новым окном.

Наблюдаемость

ТипПравило
Logsresync-started / window-progress / window-closed / report-built / candidate-resolved / resync-completed/-failed/-cancelled с RunId, Mode, счётчиками по AggregateType, CorrelationId
Metricshandbook_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)
TracesCorrelationId запуска → span окна → span отчёта; события consumer-а несут собственную корреляцию
Alertsокно закрыто по WindowTimeout (незавершённая re-эмиссия); кандидатов > 10 % строк справочника (частичный ресинк); рост DLQ за окно > 0; candidates_pending > 0 дольше 7 дней (забытый отчёт)
Dashboardhandbook-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-ом.

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

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

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