Files
dc-observatio/docs/architect/PCP_TGU_PLAN_STATUS_LIFECYCLE.md
Дмитрий Соловьев 87905311b1 pcp-tgu: якорь цепочки на LAID_IN, параллельные выдачи, SUPERSEDED
Базой цепочки план становится на ЗАКЛАДКЕ (LAID_IN), а не на приёмке.
После принятого плана можно отправить коррекцию — она тоже примется;
несколько выдач «в полёте» по одному КА теперь норма, а не конфликт.

- ACCEPT больше не двигает currentBasePlanId/chainVersion и не запирает
  слот — только переводит план в AWAITING_LAYIN.
- Новый позитивный топик layin-confirmed + LayinConfirmedConsumer →
  handleLayinConfirmed: AWAITING_LAYIN → LAID_IN, план становится базой
  (chainVersion++), конкуренты на пересекающемся окне гасятся в новый
  терминальный статус SUPERSEDED. Гард ±10 мин (LAYIN_MIN_SEPARATION_MINUTES):
  нельзя заложить план со стартом в пределах 10 мин от уже заложенного (409).
- «В полёте» считается из статусов планов (ISSUING/WAITING_DECISION), а не
  из единственного active-слота; снят гард «идёт выдача другого плана».
  IssueDuePlanWorker гейтится hasBlockingInFlight, базу нового плана берёт
  как latestCommittedPlanId; grace больше не двигает базу.
- PlanProjectionService якорит по самому дальнему committed-окну
  {ISSUING, WAITING_DECISION, AWAITING_LAYIN, LAID_IN}.
- REJECT ставит паузу только если по КА не осталось живых кандидатов.
- CreateDraftUseCase: replace-черновик наследует ПРЕДШЕСТВЕННИКА исходного
  плана (base.basePlanId), а не сам план — коррекция занимает тот же слот;
  fork-forward по-прежнему ссылается на базу. Идемпотентность черновиков —
  по слоту (КА + окно), а не по basePlanId.
- Тест-контроллер: POST /test/tgu/plans/{id}/confirm-layin (через BFF
  pcp-ui-service) — имитация события «план заложен».
- Док PCP_TGU_PLAN_STATUS_LIFECYCLE обновлён (LAID_IN-переход, SUPERSEDED).
2026-06-04 16:13:02 +03:00

14 KiB
Raw Permalink Blame History

Жизненный цикл статусов плана ТГУ

Справочник по статусам business-plan в расписании ТГУ: когда наступает каждый статус, что он означает и какой переход его порождает. Источник правды — enum PlannedPlanStatus; переходы — в service/IssueDuePlanWorker, service/HandlePlanDecisionUseCase, service/CreateDraftUseCase, service/CreateChainPlanUseCase, dto/PlanResponse.

Эти же статусы (кроме «зарезервированных», см. ниже) показаны в легенде вкладки «Планы» (tguStatus.ts) и задают цвет плашек на таймлайне.

Диаграмма переходов (упрощённо)

                         создать черновик
   (нет плана) ───────────────────────────────►  DRAFT
                                                    │ promote («Применить») / авто-выдача
                                                    ▼
   проекция будущего  ····(виртуально)····►  PLANNED ──(подошёл срок, авто-выдача)──►  ISSUING
                                                                                        │ BPMN-процесс стартовал
                                                                                        ▼
                                                                                 WAITING_DECISION
                                            ACCEPTED-решение │           │ REJECTED-решение
                                                             ▼           ▼
                                                      AWAITING_LAYIN   REJECTED  (цепочка на паузе)
                                                             │
                            ┌────────────────────────────────┼────────────────────────────────┐
                  событие «заложен»              событие «не заложен»            событие «не принят»
                            ▼                                ▼                              ▼
                        LAID_IN                        LAYIN_FAILED                   NOT_ACCEPTED
                  (база цепочки; конкуренты         (срочная перевыдача)          (срочная перевыдача)
                   того же окна → SUPERSEDED)

   при сбое старта BPMN из ISSUING ─────────────────────────────────────────►  START_AMBIGUOUS

Статусы

DRAFT — черновик

  • Когда. Оператор создаёт план через «Создать план» (CreateChainPlanUseCase) или «Добавить»/ редактирование (CreateDraftUseCase).
  • Что значит. План в стадии правки. Персистится, но не является головой цепочки, не попадает в проекцию/read-путь как запланированный слот и не выдаётся авто-воркером.
  • Дальше. Становится активным только после promote («Применить») — переходит в ISSUING. Удаление черновика (abandon) убирает его и клон-миссию.
  • В UI помечен как «ЧЕРНОВИК», виден только при включённом режиме «Черновики».

PLANNED — проекция будущего (виртуальный)

  • Когда. Вычисляется на лету PlanProjectionService/PlanResponse.projected() при чтении расписания. Никогда не пишется в БД.
  • Что значит. Предполагаемый будущий слот цепочки (следующие ЗРВ) — «как пойдёт цепочка, если ничего не менять». Не несёт basePlanId, стартом цепочки не считается.
  • Дальше. Когда подходит срок выдачи, по этому слоту авто-воркер материализует реальный план сразу в ISSUING (materializeIssuingPlaninsertIssuingPlan).
  • В UI виден только при включённом тумблере «Возможные будущие планы».

ISSUING — идёт выдача

  • Когда. IssueDuePlanWorker начинает выдачу: promote черновика (prepareDraftPromotion), ручная (пере)выдача конкретного плана (prepareSpecificPlan) или авто-материализация головы проекции (materializeIssuingPlan).
  • Что значит. Запущен BPMN-процесс выдачи плана на борт; создан attempt со статусом STARTING.
  • Дальше. Успешный старт процесса → WAITING_DECISION (markStarted). Сбой старта BPMN → START_AMBIGUOUS (markAmbiguous).

WAITING_DECISION — ожидает решения

  • Когда. BPMN-процесс выдачи успешно стартовал, attempt → STARTED (markStarted).
  • Что значит. План выдан, ждём решения оператора/борта: ACCEPTED либо REJECTED.
  • Дальше.
    • ACCEPTED → AWAITING_LAYIN (acceptPlan).
    • REJECTED → REJECTED (rejectPlan).
    • Grace-окно. Если решения нет дольше waitingDecisionGraceMinutes после startTime, цепочка «молчаливо» едет дальше: якорь сдвигается на этот план, блокировка снимается, но статус остаётся WAITING_DECISION (tryAdvancePastUnconfirmedPlan). Запоздалое решение запишется на attempt и ушедшую вперёд цепочку не тронет. Поэтому в UI кнопки решения гасятся после grace.

AWAITING_LAYIN — принят, ждёт подтверждения закладки

  • Когда. Пришло решение ACCEPTED (acceptPlan).
  • Что значит. План принят и ждёт закладки. ACCEPT больше НЕ делает план базой и не двигает chainVersion — это разъединено намеренно: после приёмки можно отправить и принять коррекцию того же окна, поэтому по одному окну может одновременно «лететь» несколько AWAITING_LAYIN/ WAITING_DECISION. Базой и версией цепочки управляет только закладка (LAID_IN). На ACCEPT запускается джоб обновления слотов принятого плана (acceptedPlanSlotsUpdateJobService.enqueue).
  • Дальше. При подтверждении закладки → LAID_IN (становится базой). При негативе — LAYIN_FAILED/NOT_ACCEPTED. Если базой стала другая коррекция того же окна — этот план гасится в SUPERSEDED.
  • В легенде это «принятый, ждёт закладки» статус (зелёный). Перевыдача/коррекция в свой слот ещё возможна, пока до старта больше layinFreezeMinutes (после — «точка невозврата», только fork-forward).

REJECTED — отклонён

  • Когда. Пришло решение REJECTED (rejectPlan).
  • Что значит. План признан неправильным. Авто-перевыдача не взводится (повторять тот же план бессмысленно). Цепочка ставится на паузу (pausedAfterReject = true) — только если по КА не осталось других живых кандидатов (в полёте/приняты, но не заложены): при нескольких выдачах отказ одной не глушит цепочку, пока жив другой кандидат. На паузе проекция пуста, воркер ничего не выдаёт. Базовый план не трогается.
  • Дальше. Оператор вручную создаёт исправленный план; его promote/ручная выдача снимают паузу.
  • В UI у КА с паузой рисуется пометка «⏸ на паузе».

NOT_ACCEPTED — не принят (негативный фидбек)

  • Когда. Пришло внешнее событие «не принят» по текущему базовому плану в AWAITING_LAYIN (handleNotAcceptedapplyNegativeFeedback).
  • Что значит. Терминальный негатив по базовому плану. В отличие от REJECTED — это технический негатив, поэтому запускается срочная перевыдача на ближайшую ЗРВ (reissueOnNearestWindow).
  • Событие, пришедшее не по текущему базовому плану (устаревшее/дубль), игнорируется.

LAYIN_FAILED — не заложен (негативный фидбек)

  • Когда. Пришло внешнее событие «не заложен» по текущему базовому плану в AWAITING_LAYIN (handleLayinFailedapplyNegativeFeedback).
  • Что значит. Терминальный негатив: закладка не удалась. Поведение сейчас идентично NOT_ACCEPTEDсрочная перевыдача на ближайшую ЗРВ.

START_AMBIGUOUS — неопределённый старт

  • Когда. Из ISSUING не удалось достоверно стартовать BPMN-процесс выдачи (исключение в startPreparedAttemptmarkAmbiguous); attempt → AMBIGUOUS.
  • Что значит. Неизвестно, ушёл план на борт или нет — требуется ручной разбор оператором. Опасный статус, в UI выделен пунктиром.

LAID_IN — заложен (терминальный успех, база цепочки)

  • Когда. Пришло внешнее событие «план заложен» по плану в AWAITING_LAYIN (LayinConfirmedConsumerhandleLayinConfirmed, топик layin-confirmed).
  • Что значит. Подтверждённая закладка — именно она делает план базой цепочки (state.currentBasePlanId = plan.planId, chainVersion += 1, снимается пауза/срочная перевыдача). «Точка невозврата»: дальше план меняется только через fork-forward (гарды CreateDraftUseCase, assertReissuableInSlot).
  • Supersede конкурентов. Все ещё не заложенные планы КА (ISSUING/WAITING_DECISION/AWAITING_LAYIN) с окном, пересекающим окно заложенного, переводятся в SUPERSEDED (supersedeCompetingPlans).
  • Событие по плану не в AWAITING_LAYIN (устаревшее/дубль/уже вытеснён) — игнорируется.
  • В легенде — зелёный, темнее AWAITING_LAYIN.

SUPERSEDED — вытеснён заложенной коррекцией (терминал)

  • Когда. По тому же окну КА другой план ушёл в LAID_IN, а этот был конкурирующей выдачей/ коррекцией (supersedeCompetingPlans).
  • Что значит. Терминал, цепочку не двигает. Запоздавшее решение по его attempt записывается, но игнорируется (план уже не in-flight). Базовый план не трогается.

Зарезервированные / не возникающие сейчас статусы

  • ACCEPTED — в enum присутствует, но приёмка (acceptPlan) переводит план сразу в AWAITING_LAYIN, минуя ACCEPTED. Фактически это легаси-значение, нигде не присваивается; в легенду вкладки «Планы» намеренно не вынесено.

Связанные конфиги (PlanningProperties)

  • waitingDecisionGraceMinutes — сколько ждём решение после старта, прежде чем «молчаливо» ехать дальше (см. WAITING_DECISION).
  • layinFreezeMinutes — за сколько минут до старта окно выдачи замораживается («точка невозврата»): перевыдача в свой слот запрещена, только fork-forward.
  • notificationBeforeStartMinutes — за сколько до старта авто-воркер начинает выдачу головы цепочки.

См. также