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

Background Job: talent-initial-import (первичная загрузка / bootstrap из Talent Chessboard)

Разовый миграционный job контекста chessboard: наполняет пустую реплику шестью агрегатами Talent Chessboard перед тем, как непрерывный consumer talent-chessboard-sync возьмёт актуализацию на себя. Объявлен сценарием UC-CHS-005 A8 «Начальная загрузка». Дома целевых полей и инвариантов — domain-доки (здесь не переописываются, только маппинг источника).

Назначение

Bootstrap первичного наполнения реплики шахматки на старте платформы (или полного пересинка проекта): разово прочитать полный набор агрегатов Talent (Developer / Project / Building / LayoutGroup / Layout / PropertyObject) и создать их как реплики PIN (SyncSource = TalentChessboard, ReplicaVersion/LastSyncedAt заполнены), идемпотентно, в порядке родитель→дочерний. Реплика — гибрид данных Talent и PIN. Job применяет тот же маппинг полей и инварианты, что и непрерывный consumer, поэтому результат bootstrap и потоковой синхронизации побитно совпадает. После завершения управление передаётся непрерывному consumer-у; дальнейшая актуализация — только событиями talent.chessboard.*.

Классификация

ПолеЗначение
Сервисchessboard (проект Pin.Chessboard.Background)
Owning teamPIN Platform Core
Статус документаdraft
Область действияРазовый bootstrap реплики Talent; таблицы chessboard.* (все шесть агрегатов) + оперативные chessboard.talent_import_runs/talent_import_pages + очередь исходящих событий
Change contextgreenfield (миграционный инструмент запуска платформы)
Тип jobOne-off (миграционный / bootstrap; не по расписанию)
TriggerРучной запуск администратором платформы (admin API, ADR-0053) либо CLI-флаг Pin.Chessboard.Background --run-initial-import
Критичностьhigh (без реплики нет каталога/шахматки; но запускается контролируемо, вне горячего пути)

Расписание И Locking

АспектПравило
ScheduleНет (one-off, инициируется оператором); типично — один раз при заведении окружения либо точечный пересинк проекта (ProjectIdFilter)
Distributed lockPostgreSQL advisory lock (без Redis — ADR-0056; durable, переживает рестарт pod-а) ключ Pin:lock:job:chessboard-talent-initial-import; один запуск на кластер; acquire timeout 5 мин → отказ без ретрая, если уже идёт
Batch sizePageSize (default 500 объектов на страницу; см. входную модель)
Concurrency внутри запускаСтраницы работ раскладываются в очередь talent_import_pages; несколько pod-ов тянут по одной странице под построчной блокировкой с пропуском занятых (по Stage, PageNo) — параллелизм без двойной обработки и без Redis (ADR-0056); барьер этапа: страницы дочернего этапа ставятся в очередь только после Done родительского (порядок developers→projects→buildings→layout-groups→layouts→propertyObjects)
Max durationДо 6 ч на полный bootstrap; лизинг страницы 10 мин (протухший InProgress переклаймится); отмена проверяется на каждой странице и I/O-вызове
Feature flagchessboard.talent-sync.enabled = true (общий с consumer; при false job не стартует)

Входная Модель TalentInitialImportRequest

ПолеТипОбязательностьИсточникОписание
TenantIdНе применяетсяJob кросс-tenant: пишет под system-actor без tenant-фильтра, TenantId каждой записи выводится из данных (INV-DEV-1/INV-PRJ-1/INV-BLD-1/INV-CHS-004), как в consumer
ModeTalentImportModeДаadmin-входСпособ чтения источника (enum ниже)
DeveloperIdFilterGuid?Нетadmin-входОграничить bootstrap одним застройщиком (ExternalTalentDeveloperId) — частичный пересинк
ProjectIdFilterGuid?Нетadmin-входОграничить одним проектом (Runbook «пересинк проекта из Talent»)
PageSizeintНет (default 500)admin-входРазмер страницы API/replay; 1..2000
DryRunboolНет (default false)admin-входПрогон без мутаций и outbox: только валидация и подсчёт (TalentInitialImportResult)
CorrelationIdGuidДагенерируетсяСквозной trace всех страниц и исходящих pin.chessboard.*

Enum TalentImportMode (хранится text, .HasConversion<string>())

ЗначениеОписание
TopicReplayТопики Talent compacted (по одному последнему snapshot на ключ) → сброс offset consumer-группы pin-chessboard-talent-sync в 0 и повторное потребление тем же путём; job только оркеструет reset + барьеры этапов
ApiPullТопики не compacted → постраничный HTTP-импорт по API Talent (developers/projects/buildings/layouts list, property-objects list/by-ids — имена assumption), применяет тот же маппинг снапшотов, что и непрерывный consumer

Оперативное состояние (checkpoint, job-owned)

Не доменные агрегаты — операционные типизированные таблицы прогресса (позволяют resume и параллелизм).

СущностьКлючевые поляНазначение
TalentImportRunEntityId, Mode, Status (Running/Completed/Failed/Cancelled), StartedAt, FinishedAt, CorrelationId, счётчики по этапамОдин запуск; anti-двойной-старт вместе с advisory lock
TalentImportPageEntityId, RunId FK, Stage (TalentImportStage enum: Developers..PropertyObjects), PageNo, Cursor (opaque токен API), Status (Pending/InProgress/Done/Failed), LeaseUntil, ItemsApplied, ItemsFailedЕдиница работы; claim через ForUpdateSkipLocked; идемпотентный resume по Status
TalentImportFailedItemEntityId, RunId, Stage, ExternalId, ErrorCode, Payload (typed snapshot), CreatedAtКарантин poison-item-ов (не валит страницу); ручной разбор

Предусловия И Постусловия

Предусловия (до запуска).

  • Feature flag chessboard.talent-sync.enabled = true; advisory lock свободен (иначе — отказ без ретрая).
  • Источник Talent доступен: для ApiPull — HTTP API list/by-ids; для TopicReplay — топики compacted и право сброса offset consumer-группы (иначе — CHESSBOARD_IMPORT_SOURCE_UNAVAILABLE).
  • Реплика пуста (первичный bootstrap) либо запускается точечный пересинк (DeveloperIdFilter/ProjectIdFilter); фильтры взаимоисключающие.

Постусловия (после Completed).

  • Все шесть агрегатов Talent из scope запуска материализованы репликами PIN (SyncSource = TalentChessboard, ReplicaVersion/LastSyncedAt заполнены), в порядке родитель→дочерний (барьеры этапов соблюдены).
  • Состояние реплики побитно совпадает с результатом потоковой синхронизации того же scope (тот же маппинг, инварианты, unknown-enum-fallback-и) — parity с consumer.
  • Опубликованы те же pin.chessboard.* события, что у consumer (+ price/status для сидирования истории и импортируемых Sold); поднят по одному ProjectAggregatesRecalcRequestedEvent на затронутый проект.
  • TalentImportRunEntity.Status = Completed, advisory lock освобождён; poison-item-ы — в TalentImportFailedItemEntity (страница/запуск не падают). При DryRun = true мутаций и outbox нет — только счётчики.
  • Дальнейшая актуализация передана непрерывному consumer-у (частичный флаг снят оператором).

Алгоритм

Оркестратор планирует этапы и барьеры; тело — цикл обработки одной страницы (повторяемая единица; параллелится между pod-ами).

  1. Контекст и доступ. Выполняет system-actor (тот же, что у consumer); проверяется флаг chessboard.talent-sync.enabled (выключен → job не стартует); захватывается advisory lock (занят → отказ без ретрая); создаётся/загружается запуск TalentImportRunEntity (существующий Running → resume того же запуска, идемпотентность рестарта). CorrelationId — из входа.
  2. Проверка входа. Mode — валидный enum; PageSize в 1..2000; DeveloperIdFilter/ProjectIdFilter не оба заданы (взаимоисключающие scope); доступность источника — проверка Talent API (ApiPull) либо наличие топиков и права сброса offset (TopicReplay), иначе → CHESSBOARD_IMPORT_SOURCE_UNAVAILABLE. Существование сущностей не проверяется — операция upsert.
  3. Планирование этапов. По этапам в порядке родитель→дочерний (Developers → Projects → Buildings → LayoutGroups → Layouts → PropertyObjects): для текущего этапа — постраничный обход источника (ApiPull: list(cursor, PageSize, filter); TopicReplay: чтение партиций с offset 0), запись страниц работы TalentImportPageEntity(Status = Pending). Дочерний этап планируется только после того, как все страницы родителя Done (барьер гарантирует, что родитель уже в реплике до дочернего — без CHESSBOARD_SYNC_PARENT_NOT_FOUND). Страница берётся в работу под построчной блокировкой с пропуском занятых (первая Pending по Stage, PageNo) → InProgress, LeaseUntil = now + 10м.
  4. Применение элементов страницы. Для каждого элемента страницы применяется тот же маппинг снапшота Talent, что и у непрерывного consumer (маппинг поле-в-поле, enum-fallback-и reference-data.md, unknown-enum → fallback + warn + chessboard_sync_unknown_enum_total): version-guard payload.Version > entity.ReplicaVersion (INV-DEV-4/PRJ-5/BLD-5/LAY-4/LGR-4 и guard объекта недвижимости — идемпотентный skip при повторе); идемпотентность по External*Id (уникальность реплики: INV-DEV-2/INV-CHS-006 и др.); создание из снапшота при отсутствии (Status = Draft для developer/project/building; объект недвижимости — initialStatus из Talent, StatusSource = TalentSync) либо применение снапшота (только реплика-поля; PIN-поля неприкосновенны — *-SyncOwnedFields).
  5. Что меняется. Порядок с учётом FK: layoutGroup → layout → propertyObject внутри страницы PropertyObjects; создание/upsert по External*Id; денормализованные счётчики не считаются синхронно — поднимается один ProjectAggregatesRecalcRequestedEvent на затронутый ProjectId (debounce 30 с, джоб project-aggregates-recalc). Ошибка одного элемента → карантин TalentImportFailedItemEntity, страница продолжается (ItemsFailed++).
  6. Какие события фиксируются. Транзакционно (та же транзакция страницы) — те же pin.chessboard.*, что у consumer: developer-upserted, project-upserted, building-upserted, layout-group-upserted, layout-upserted, property-object-upserted; для объекта недвижимости дополнительно property-object-price-changed (начальная ценовая точка Source = TalentSync — seed истории invest-analytics) и property-object-status-changed для импортируемых уже Sold. DryRun = true → шаги 5–6 пропускаются (только счётчики).
  7. Согласованность. На страницу — одно согласованное изменение: мутации агрегатов + TalentImportPageEntity(Status = Done, ItemsApplied/Failed) + отметки идемпотентности + исходящие события — атомарно. Падение до коммита → страница остаётся InProgress, лизинг протухает, переклаймится и переигрывается (upsert + version-guard делают повтор безопасным).
  8. Результат. По завершении всех этапов — TalentImportRunEntity(Status = Completed, FinishedAt), освобождение lock; итог по агрегатам (Planned/Applied/Skipped/Failed, длительность, RunId). Оператор снимает частичный флаг и передаёт актуализацию consumer-у.

Граница Транзакции

ЭлементПравило
Unit of workОдна транзакция на страницу; не на весь запуск (иначе долгая блокировка и невозможность resume)
Сохраняемое состояниеАгрегаты-реплики + TalentImportPageEntity(Done) + отметки идемпотентности + исходящие события — атомарно; TalentImportFailedItemEntity — в той же транзакции
External callsЧтение Talent (API/replay) — до транзакции страницы (сеть вне коммита); публикация событий — после коммита
Частичная ошибкаPoison-item → карантин, страница продолжается; сбой страницы → откат страницы, переклайм по лизингу; сбой этапа не трогает Done-страницы (resume)

Идемпотентность И Retry

СценарийПравило
Job restartResume: TalentImportRunEntity(Running) подхватывается; Done-страницы пропускаются, Pending/протухший InProgress переигрываются
Duplicate item / повторный запускUpsert по External*Id + монотонный ReplicaVersion-guard → повторное применение — no-op (тот же контракт, что replay топика у consumer)
Повторяемая ошибка (источник/БД недоступны)Страница InProgress не помечается Done; лизинг протухает → переклайм; infra-исключения — exponential backoff, отмена уважается
Poison item (жёсткий диапазон, дубль номера, конфликт статуса)TalentImportFailedItemEntity + chessboard_import_failed_items_total + алерт; страница/запуск не падают; ручной разбор (Runbook), затем точечный replay
Гонка с continuous-consumerБезопасна: ReplicaVersion-guard дедуплицирует двойное применение; рекомендуемая последовательность — bootstrap, затем поток; одновременная работа корректна по идемпотентности

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

ТипПравило
Logsimport-started / stage-planned / page-claimed / page-applied / item-quarantined / stage-completed / import-completed/import-failed с RunId, Stage, PageNo, ExternalId, ItemsApplied/Failed, CorrelationId. PII нет (данные публичны/internal)
Metricschessboard_import_running (gauge 0/1), chessboard_import_pages_total{stage,status}, chessboard_import_items_total{aggregate,result}, chessboard_import_page_duration_ms, chessboard_import_throughput_items_per_sec, chessboard_import_failed_items_total, переиспользуется chessboard_sync_unknown_enum_total{enum,value}
TracesCorrelationId запуска → span этапа → span страницы → DB/outbox-spans; проброс в исходящие pin.chessboard.*
SLO циклаПолный bootstrap реплики (envelope ADR-0056: ~10k проектов / ~1M объектов недвижимости) < 2 ч при агрегатной пропускной ≥ 2000 propertyObjects/s; page p99 < 3 с; после хендовера лаг consumer-а < 5 мин (SLO README)
Alertschessboard_import_running = 1 без роста pages_total{status=Done} > 10 мин (stuck); chessboard_import_failed_items_total > 0; длительность запуска > SLO; рост unknown-enum
Dashboardchessboard-overview (панель bootstrap: прогресс по этапам, throughput, карантин)

Связанный Runbook

СценарийRunbook
Первичный запуск окружения«bootstrap реплики Talent»: снять частичный флаг, запустить job (Mode по свойству топиков), дождаться Completed, передать поток consumer-у
Пересинк одного проекта«пересинк проекта из Talent» (README chessboard): запуск с ProjectIdFilter (после ручного resolution конфликтов, area admin)
Карантин poison-item-овРазбор TalentImportFailedItemEntity → исправление данных в Talent → точечный replay
Job завис (protухший lock/лизинг)chessboard-overview: проверить держателя advisory lock, дождаться/сбросить лизинг страниц, resume

Тесты

ТестОжидание
Empty sourceПустой Talent → Completed, 0 применённых, идемпотентно
Порядок родитель→дочернийБарьер этапов: объекты недвижимости применяются только после зданий/проектов/застройщиков (нет CHESSBOARD_SYNC_PARENT_NOT_FOUND)
Duplicate runПовторный запуск на наполненной БД — no-op (version-guard); состояние не меняется
Partial failure / resumeПадение на середине этапа → рестарт продолжает с Pending-страниц, Done не переигрываются
Poison itemОбъект недвижимости вне диапазона → карантин + метрика, страница и запуск завершаются успешно
Parity с consumerBootstrap и потоковый синк одного проекта дают идентичное состояние реплики и одинаковые pin.chessboard.*
DryRunМутаций и outbox нет; TalentInitialImportResult содержит корректные счётчики
Cancellationотмена останавливает после текущей страницы; Run = Cancelled, Done-страницы сохранены

Известные Ограничения И Assumptions

  • Имена топиков/HTTP-эндпоинтов и модель PropertyObject Talent — assumption до подтверждения schema registry / API Talent (README chessboard «Известные Ограничения»).
  • TopicReplay применим только если топики Talent compacted; иначе — ApiPull.
  • Job не публикует профили (Status = Draft у developer/project/building) — публикация остаётся решением ПИН/застройщика (INV-DEV-5/INV-PRJ-3); объекты недвижимости берут статус из Talent (Free/Sold/Hidden).
  • Активные холды CRM отсутствуют на пустой реплике; при пересинке конфликт Sold vs Reserved/Booked обрабатывается как в consumer (CHESSBOARD_SYNC_STATUS_CONFLICT → карантин).

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

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

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