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 team | PIN Platform Core |
| Статус документа | draft |
| Область действия | Разовый bootstrap реплики Talent; таблицы chessboard.* (все шесть агрегатов) + оперативные chessboard.talent_import_runs/talent_import_pages + очередь исходящих событий |
| Change context | greenfield (миграционный инструмент запуска платформы) |
| Тип job | One-off (миграционный / bootstrap; не по расписанию) |
| Trigger | Ручной запуск администратором платформы (admin API, ADR-0053) либо CLI-флаг Pin.Chessboard.Background --run-initial-import |
| Критичность | high (без реплики нет каталога/шахматки; но запускается контролируемо, вне горячего пути) |
Расписание И Locking
| Аспект | Правило |
|---|
| Schedule | Нет (one-off, инициируется оператором); типично — один раз при заведении окружения либо точечный пересинк проекта (ProjectIdFilter) |
| Distributed lock | PostgreSQL advisory lock (без Redis — ADR-0056; durable, переживает рестарт pod-а) ключ Pin:lock:job:chessboard-talent-initial-import; один запуск на кластер; acquire timeout 5 мин → отказ без ретрая, если уже идёт |
| Batch size | PageSize (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 flag | chessboard.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 |
Mode | TalentImportMode | Да | admin-вход | Способ чтения источника (enum ниже) |
DeveloperIdFilter | Guid? | Нет | admin-вход | Ограничить bootstrap одним застройщиком (ExternalTalentDeveloperId) — частичный пересинк |
ProjectIdFilter | Guid? | Нет | admin-вход | Ограничить одним проектом (Runbook «пересинк проекта из Talent») |
PageSize | int | Нет (default 500) | admin-вход | Размер страницы API/replay; 1..2000 |
DryRun | bool | Нет (default false) | admin-вход | Прогон без мутаций и outbox: только валидация и подсчёт (TalentInitialImportResult) |
CorrelationId | Guid | Да | генерируется | Сквозной 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 и параллелизм).
| Сущность | Ключевые поля | Назначение |
|---|
TalentImportRunEntity | Id, Mode, Status (Running/Completed/Failed/Cancelled), StartedAt, FinishedAt, CorrelationId, счётчики по этапам | Один запуск; anti-двойной-старт вместе с advisory lock |
TalentImportPageEntity | Id, RunId FK, Stage (TalentImportStage enum: Developers..PropertyObjects), PageNo, Cursor (opaque токен API), Status (Pending/InProgress/Done/Failed), LeaseUntil, ItemsApplied, ItemsFailed | Единица работы; claim через ForUpdateSkipLocked; идемпотентный resume по Status |
TalentImportFailedItemEntity | Id, 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-ами).
- Контекст и доступ. Выполняет system-actor (тот же, что у consumer); проверяется флаг
chessboard.talent-sync.enabled (выключен → job не стартует); захватывается advisory lock (занят → отказ
без ретрая); создаётся/загружается запуск TalentImportRunEntity (существующий Running → resume того же
запуска, идемпотентность рестарта). CorrelationId — из входа.
- Проверка входа.
Mode — валидный enum; PageSize в 1..2000; DeveloperIdFilter/ProjectIdFilter не
оба заданы (взаимоисключающие scope); доступность источника — проверка Talent API (ApiPull) либо наличие
топиков и права сброса offset (TopicReplay), иначе → CHESSBOARD_IMPORT_SOURCE_UNAVAILABLE. Существование
сущностей не проверяется — операция upsert.
- Планирование этапов. По этапам в порядке родитель→дочерний
(
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м.
- Применение элементов страницы. Для каждого элемента страницы применяется тот же маппинг снапшота 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).
- Что меняется. Порядок с учётом FK:
layoutGroup → layout → propertyObject внутри страницы
PropertyObjects; создание/upsert по External*Id; денормализованные счётчики не считаются синхронно —
поднимается один ProjectAggregatesRecalcRequestedEvent на затронутый ProjectId (debounce 30 с, джоб
project-aggregates-recalc). Ошибка одного элемента → карантин TalentImportFailedItemEntity, страница
продолжается (ItemsFailed++).
- Какие события фиксируются. Транзакционно (та же транзакция страницы) — те же
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 пропускаются (только счётчики).
- Согласованность. На страницу — одно согласованное изменение: мутации агрегатов +
TalentImportPageEntity(Status = Done, ItemsApplied/Failed) + отметки идемпотентности + исходящие события —
атомарно. Падение до коммита → страница остаётся InProgress, лизинг протухает, переклаймится и
переигрывается (upsert + version-guard делают повтор безопасным).
- Результат. По завершении всех этапов —
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 restart | Resume: 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, затем поток; одновременная работа корректна по идемпотентности |
Наблюдаемость
| Тип | Правило |
|---|
| Logs | import-started / stage-planned / page-claimed / page-applied / item-quarantined / stage-completed / import-completed/import-failed с RunId, Stage, PageNo, ExternalId, ItemsApplied/Failed, CorrelationId. PII нет (данные публичны/internal) |
| Metrics | chessboard_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} |
| Traces | CorrelationId запуска → 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) |
| Alerts | chessboard_import_running = 1 без роста pages_total{status=Done} > 10 мин (stuck); chessboard_import_failed_items_total > 0; длительность запуска > SLO; рост unknown-enum |
| Dashboard | chessboard-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 с consumer | Bootstrap и потоковый синк одного проекта дают идентичное состояние реплики и одинаковые 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 → карантин).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы