Раньше вся будущая цепочка планов материализовалась в planned_plan как PLANNED-строки, а отдельный воркер пересчёта (RebuildPlanScheduleUseCase + PlanRecalculationJob) держал её в актуальном состоянии и гасил выпавшие планы в SUPERSEDED/EXPIRED. Это плодило вложенные планы, гонки пересчёта и мусор в БД. Новая модель: в БД живёт только выданное (история навсегда), будущее — проекция, вычисляемая по запросу из ЗРВ. Перекрывать (supersede) и протухать (expire) больше нечего. Бэкенд: - Удалены TrackedPlan (entity/source/status + repo) и PlanRecalculationJob (entity/repo/service/worker) — материализация и воркер пересчёта больше не нужны. - Удалён RebuildPlanScheduleUseCase; будущее строит новый PlanProjectionService поверх PlanCalculationService. - PlanQueryService отдаёт проекцию (выданное из БД + будущее на лету). - Новый флоу реакции на смену параметров станции: StationParamsChangedConsumer + HandleStationParamsChangedUseCase (срочный перевыпуск через planning_state). - Новые консьюмеры обратной связи исполнения: LayinFailedConsumer, PlanNotAcceptedConsumer (+ DTO PlanExecutionFeedbackMessage, StationParamsChangedMessage). - PlannedPlanStatus: убраны мёртвые SUPERSEDED/EXPIRED; статусы выровнены под жизненный цикл выдачи/закладки. Миграции (ddl-auto: validate): - V7 drop tracked_plan - V8 drop plan_recalculation_job - V9 planning_state: поля срочного перевыпуска - V10 снять unique-ограничение бизнес-ключа planned_plan - V11 удалить легаси PLANNED-строки (материализованное будущее) - V12 удалить легаси SUPERSEDED/EXPIRED и столбец superseded_by_plan_id Тесты: удалены JobRerunFlowTest и RebuildPlanScheduleUseCaseTest; добавлены PlanProjectionServiceTest, PlanQueryServiceTest, IssueDuePlanWorkerOnDemandTest, HandleStationParamsChangedUseCaseTest, VisibilityPayloadParserTest. Скорректированы PlanIssueAndDecisionFlowTest, PlanCalculationServiceTest, VisibilityFlowTest и др. V11/V12 необратимы — перед миграцией на проде снять дамп planned_plan.
32 KiB
Задача: pcp-tgu-service — «храним только выданное» + закладочный флоу
ТЗ для кодинг-агента и для приёмки. Раздел 0 — промпт запуска. Разделы 1–9 — что и как делаем. Раздел 10 — критерии приёмки (по ним проверяем результат). Стиль работы, границы слоёв и формат финального ответа — по корневому
AGENTS.md.
0. Промпт для запуска агента (copy-paste)
Ты работаешь в монорепозитории observatio-terrae (pcp), модуль services/pcp-tgu-service (Kotlin/Spring Boot).
Задача: перестроить хранение и выдачу планов ТГУ — перестать материализовать
будущую цепочку планов в БД (хранить только выданные планы и их историю — вечно),
считать будущее на лету как проекцию, и добавить закладочный флоу с реакцией
на два Kafka-события «не заложен» и «не принят».
Перед началом обязательно прочитай:
1. /AGENTS.md — правила работы, стиль, формат финального ответа.
2. /docs/tasks/TASK_tgu-plan-on-demand-redesign.md — это ТЗ.
3. Текущий код: services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/
(service/, entity/, repository/, controller/, integration/kafka/, config/).
Делай строго по фазам раздела 5. Всё — в одной ветке/одном PR.
Ядро расчёта цепочки (PlanCalculationService) НЕ меняем — оно уже монотонно-корректно.
После работы прогоняй ./gradlew :services:pcp-tgu-service:test из корня монорепо.
В конце дай отчёт в формате из AGENTS.md и пройдись по разделу 10 (критерии приёмки).
1. Контекст и цель
pcp-tgu-service (порт :7011) — роллинг-планировщик сеансов управления по каждому операционному КА. Поток: список КА из НСИ → окна видимости (ЗРВ) из баллистики → расчёт цепочки планов (~target 1440±240 мин, привязка 1:1 к ЗРВ, строгая монотонность) → выдача дозревшего плана через короткий BPMN → решение из Kafka. UI (через агрегатор :7008) рисует таймлайн планов.
Проблема сейчас. Таблица planned_plan материализует всю будущую цепочку как PLANNED-строки и переписывает её на каждое изменение хэша ЗРВ. Это копит «лишнее»: пачки SUPERSEDED/EXPIRED на каждый пересчёт, рост без границ, отдельная машинерия plan_recalculation_job, и целый класс багов рассинхрона цепочки со снапшотом («схлопывание в SUPERSEDED», «зомби-PLANNED»). При этом будущая цепочка — детерминированная проекция от (снапшот ЗРВ + базовый план + конфиг).
Цель.
- Хранить в
planned_planтолько планы, перешедшие в выдачу, и их историю — вечно (ретеншн не делаем). - Будущее (не выданное) — не хранить вовсе, считать на лету как проекцию (в т.ч. для UI).
- Добавить закладочный флоу: после
ACCEPTEDплан оптимистично считается заложенным; реагируем только на явный негатив — два раздельных Kafka-события «не заложен» и «не принят», оба пока ведут себя одинаково (срочно выдать план на ближайшую ЗРВ), но разнесены для будущего расхождения. - Реагировать на изменение параметров станций (Kafka-событие): станция могла выйти из строя — нужно заново запросить ЗРВ у баллистики по затронутым КА и пересобрать цепочку. В on-demand-модели это сводится к форсу
visibility_refresh_job→ новый снапшот → проекция пересчитается сама. - Снести мёртвый код (
tracked_plan) и ставшую ненужной машинерию пересчёта (plan_recalculation_job).
Масштаб: 100–150 КА, горизонт планирования до 10 суток, деплой 2–3 реплики (горизонтальное масштабирование уже обеспечено координацией через БД: FOR UPDATE SKIP LOCKED + pessimistic-lock на КА).
2. Границы задачи (scope)
В scope:
- Перестроение хранения/выдачи планов в
pcp-tgu-service(backend, Kotlin). - Новый сервис проекции; переписанный воркер выдачи; read-эндпоинт, отдающий persisted ∪ проекцию.
- Новые статусы плана и закладочный флоу с двумя новыми Kafka-консьюмерами.
- Удаление мёртвого кода и снятие
plan_recalculation_job. - Миграции БД, изменения конфига (
application.yml+config-repo/pcp-tgu-service.yaml). - Юнит-тесты на новую логику.
Вне scope (без отдельного запроса):
- Фронтенд
pcp-tgu-ops-uiи агрегаторpcp-ui-service— контракт/api/plansсохраняем неизменным, фронт не трогаем. - Изменение ядра расчёта
PlanCalculationService(кроме переиспользования; алгоритм не меняем). - BPMN-схемы (
issueSatellitePlan,updateAcceptedPlanSlots) — не меняем. - Источник/продюсер новых Kafka-событий «не заложен»/«не принят» — мы только консьюмеры; контракт ниже фиксируем.
- Ретеншн/архивация — НЕ делаем (выданное храним вечно).
3. Текущая архитектура (точки изменения)
- PlannedPlanEntity /
planned_plan— хранит всю цепочку, upsert по бизнес-ключу(spacecraft, start, end, kpp). - RebuildPlanScheduleUseCase — пересобирает цепочку:
calculatePlans/After→ upsert →supersedeObsoleteFuturePlans→expireMissedPlans. - PlanCalculationService — ядро, не трогаем.
- SpacecraftVisibilitySnapshotService — на изменение хэша ставит
plan_recalculation_job. - PlanRecalculationWorker /
PlanRecalculationJobService/plan_recalculation_job— снимаем целиком. - IssueDuePlanWorker — сейчас сканирует
PLANNED(findDueForUpdate). Переписываем на on-demand. - HandlePlanDecisionUseCase — обработка
ACCEPTED/REJECTED;promoteNextPlanOrEnqueueRebuildзавязан на хранимыеPLANNED→ переписываем. - PlanQueryService / PlanController — read-путь, дополняем проекцией.
- Мёртвый код: TrackedPlanEntity,
TrackedPlanStatus,TrackedPlanSource,TrackedPlanJpaRepository— на репозиторий нет ссылок.
4. Целевая архитектура
planned_planпополняется строкой только в момент выдачи (статусISSUING). Дальше строка живёт по жизненному циклу выдачи и хранится вечно.- Будущее не хранится.
PlanProjectionService.project(spacecraftId)считает цепочку на лету из последнего снапшота ЗРВ + текущей базы + конфига, переиспользуяPlanCalculationService. - Выдача ([IssueDuePlanWorker]) идёт по активным КА, считает голову проекции; если голова «дозрела» (
issue_not_before ≤ now) или взведёнurgent_reissue_from— материализует ровно эту голову вISSUINGи выдаёт. НикакихPLANNED-строк в БД. - Read ([PlanQueryService]) отдаёт
persisted (вся история) ∪ проекция будущего(виртуальныйPLANNED, в БД не пишется), дедуп по бизнес-ключу. Контракт/api/plansнеизменен. - Закладочный флоу:
ACCEPTED→AWAITING_LAYIN(оптимистично = база, цепочка идёт дальше). Два новых события гасят план терминально (LAYIN_FAILED/NOT_ACCEPTED) и взводят срочную перевыдачу на ближайшую ЗРВ с лидомnow + reissue-prep-minutes.
5. Фазы работ
Фаза 0. Чистка мёртвого кода
- Удалить
TrackedPlanEntity,TrackedPlanStatus,TrackedPlanSource,TrackedPlanJpaRepository. - Миграция
V7__drop_tracked_plan.sql:DROP TABLE IF EXISTS tracked_plan; - Убедиться, что компиляция/тесты не ссылались на эти классы.
Фаза 1. On-demand проекция, конец материализации цепочки
- Новый
PlanProjectionServiceсproject(spacecraftId): List<CalculatedPlan>иprojectHead(spacecraftId): CalculatedPlan?: читаетspacecraft_visibility_snapshot, текущую базу (spacecraft_planning_state.currentBasePlanId), считает цепочку черезPlanCalculationService(calculatePlans/calculatePlansAfter). Опционально — in-memory кэш поpayload_hash(если делаешь — инвалидируй при смене снапшота/базы; при 150 КА можно и без кэша). - Переписать
IssueDuePlanWorkerна on-demand: идём по активным КА (spacecraft_snapshot.active = true/spacecraft_planning_state), для каждого черезPlanProjectionService.projectHeadберём ближайший план; если у КА нет активной попытки (activeAttemptId == null) и (issue_not_before(head) ≤ nowили взведёнurgent_reissue_from) — материализуем голову вplanned_planсо статусомISSUINGи стартуем выдачу. Сохраняем claim-семантику с lock на КА. RebuildPlanScheduleUseCase: убрать материализацию (upsertPlannedByBusinessKey,supersedeObsoleteFuturePlans,expireMissedPlans). Логику расчёта забираетPlanProjectionService; сам use case либо удаляется, либо превращается в тонкую обёртку над проекцией (если на него ссылаетсяTestTguController— переориентировать/упростить).- Снять машинерию пересчёта: удалить
PlanRecalculationWorker,PlanRecalculationJobService,PlanRecalculationJobEntity,PlanRecalculationJobRepository; убратьenqueue(...)изSpacecraftVisibilitySnapshotService(hash-проверку оставить — она просто перестаёт что-либо ставить). МиграцияV8__drop_plan_recalculation_job.sql:DROP TABLE IF EXISTS plan_recalculation_job; PlannedPlanRepository: удалить ставшие мёртвымиupsertPlannedByBusinessKey,findAllBySpacecraftIdAndStatusAndEndTimeAfterOrderByStartTime,findAllBySpacecraftIdAndStatusAndStartTimeLessThanEqualOrderByStartTime,findTopBySpacecraftIdAndStatusAndStartTimeAfterOrderByStartTime. Оставить/добавить:lockByPlanId, выборка истории по КА и по всем для read, защита от дублей выдачи.
Фаза 2. Read-проекция для UI
PlanQueryService.getAllPlans/getPlansBySpacecraftId: вернутьpersisted ∪ projection. Persisted — все строкиplanned_plan(история вечна;historyDaysоставить как фильтр отображения левого края, БД не режем). Projection —PlanProjectionService.project(...)по активным КА, виртуальный статусPLANNED, дедуп: проекционный план не добавляем, если его бизнес-ключ уже есть среди persisted.PlanResponse: научить представлять виртуальные планы (детерминированныйplanIdиз бизнес-ключа, либо nullable — на усмотрение, но контракт JSON для фронта не ломать;status = PLANNED). Добавить фабрикуfrom(CalculatedPlan, status).
Фаза 3. Статусы и закладочный флоу
PlannedPlanStatus: добавитьAWAITING_LAYIN,LAID_IN,LAYIN_FAILED,NOT_ACCEPTED.PLANNEDоставить только как виртуальный для проекции (в БД не писать).HandlePlanDecisionUseCase(топикsatellite-plan-decision):ACCEPTED→ планACCEPTEDзатемAWAITING_LAYIN,currentBasePlanId = plan, проекция продолжается;acceptedPlanSlotsUpdateJobService.enqueue(plan.planId)(как есть).REJECTED→ план терминальноREJECTED→ общийreissueOnNearestWindow(...).- Удалить
promoteNextPlanOrEnqueueRebuild(он завязан на хранимыеPLANNED).
- Новые события (раздельные консьюмеры, общее поведение):
TopicsProperties: добавитьlayinFailedиplanNotAccepted.- DTO
PlanExecutionFeedbackMessage(eventId, spacecraftId, planId, occurredAt, reason)— пока общий на оба события. - Консьюмеры
LayinFailedConsumer,PlanNotAcceptedConsumer(groupId${spring.kafka.consumer.group-id}). - В use case — два метода
handleLayinFailed(msg)/handleNotAccepted(msg), оба идемпотентны поeventIdчерезprocessed_plan_decision_event, оба зовут общийreissueOnNearestWindow(...); статус помечаемого плана соответственноLAYIN_FAILED/NOT_ACCEPTED.
reissueOnNearestWindow(spacecraftId, failedPlanId, now)(общий для REJECTED-после-cutoff / LAYIN_FAILED / NOT_ACCEPTED):- под lock на КА: помечает план терминальным статусом (история — навечно), снимает его с роли базы (
currentBasePlanId← предыдущая валидная база илиnull), чиститactivePlanId/activeAttemptId; - ставит
spacecraft_planning_state.urgent_reissue_from = now + reissue-prep-minutes.
- под lock на КА: помечает план терминальным статусом (история — навечно), снимает его с роли базы (
IssueDuePlanWorkerurgent-path: если у КАurgent_reissue_from != null— голову проекции берём как ближайшую ЗРВ соstartTime ≥ urgent_reissue_from, материализуем и выдаём немедленно (не ждёмissue_not_before), после успешного стартаurgent_reissue_from = null.- Миграция
V9__planning_state_urgent_reissue.sql:ALTER TABLE spacecraft_planning_state ADD COLUMN IF NOT EXISTS urgent_reissue_from TIMESTAMP NULL;
Фаза 3b. Событие «изменились параметры станций»
TopicsProperties+ конфиги:topics.station-params-changed: pcp.tgu.station-params-changed.v1.- DTO
StationParamsChangedMessage(eventId, stationId?, occurredAt, reason?). - Консьюмер
StationParamsChangedConsumer(groupId${spring.kafka.consumer.group-id}). - Use case
handleStationParamsChanged(msg): идемпотентен поeventId(общий ledgerprocessed_plan_decision_event); ставитvisibility_refresh_job(VisibilityRefreshJobService.enqueue, reasonSTATION_PARAMS_CHANGED) по всем активным КА (spacecraft_snapshot.active = true). Дальше [VisibilityRefreshWorker] перечитывает ЗРВ у баллистики → новый снапшот → on-demand проекция пересобирает цепочку автоматически. stationIdв событии пока не используется для сужения (карты «станция → КА» в tgu нет; баллистика считает ЗРВ по-КА). Поле оставляем на будущее для точечного refresh; сейчас поведение — blanket refresh всех активных КА (при 150 КА дёшево).- Уже выданные/принятые планы (
ISSUING/AWAITING_LAYIN) ретроспективно не меняются — пересобирается только будущая часть цепочки (после базы). Если станция текущего плана отказала по сути — это придёт отдельным негативным событием (layin-failed/not-accepted).
Фаза 4. Конфиг
PlanningProperties+application.yml+config-repo/pcp-tgu-service.yaml:external-points-lookahead-days: 3 → 10;- новый
reissue-prep-minutes: 30; min-reissue-lead-minutes— оставить (REJECTED-до-cutoff).
TopicsProperties+ конфиги:topics.layin-failed: pcp.tgu.layin-failed.v1,topics.plan-not-accepted: pcp.tgu.plan-not-accepted.v1,topics.station-params-changed: pcp.tgu.station-params-changed.v1(с возможностью переопределить).
Фаза 5. Тесты
- Сохранить
PlanCalculationServiceTest(ядро не тронуто). - Новые:
PlanProjectionServiceTest,IssueDuePlanWorker(on-demand голова + urgent-path),HandlePlanDecisionUseCase(ACCEPTED→AWAITING_LAYIN;REJECTED→reissue), консьюмерыlayin-failed/not-accepted(идемпотентность, раздельность, общий reissue), консьюмерstation-params-changed(идемпотентность + постановка refresh по всем активным КА), read-проекция (persisted ∪ projection+ дедуп). - Удалить тесты снятой машинерии (recalc worker, supersede/expire); поправить
RebuildPlanScheduleUseCaseTest.
6. Модель данных и миграции
| Миграция | Действие |
|---|---|
V7__drop_tracked_plan.sql |
DROP TABLE IF EXISTS tracked_plan; |
V8__drop_plan_recalculation_job.sql |
DROP TABLE IF EXISTS plan_recalculation_job; |
V9__planning_state_urgent_reissue.sql |
ALTER TABLE spacecraft_planning_state ADD COLUMN IF NOT EXISTS urgent_reissue_from TIMESTAMP NULL; |
planned_plan — схему не меняем (новые значения статуса — это VARCHAR, миграция не нужна). Индексы оставляем. ddl-auto: validate — значит сущности должны точно соответствовать таблицам после миграций.
7. Конфиг (итоговые значения)
planning:
notification-before-start-minutes: 263
target-plan-duration-minutes: 1440
target-plan-duration-delta-minutes: 240
external-update-enabled: true
external-points-lookahead-days: 10 # было 3
in-memory-history-days: 14
external-update-fixed-delay-ms: 600000
notification-check-fixed-delay-ms: 30000
min-reissue-lead-minutes: 5
reissue-prep-minutes: 30 # новый
worker-lock-ttl-seconds: 300
topics:
visibility-windows-changed: pcp.tgu.visibility-windows-changed.v1
satellite-plan-decision: pcp.tgu.satellite-plan-decision.v1
layin-failed: pcp.tgu.layin-failed.v1 # новый
plan-not-accepted: pcp.tgu.plan-not-accepted.v1 # новый
station-params-changed: pcp.tgu.station-params-changed.v1 # новый
8. Контракты Kafka (входящие, мы — консьюмер)
Существующий satellite-plan-decision (PlanDecisionMessage) — без изменений.
Новые топики, общий DTO PlanExecutionFeedbackMessage:
{
"eventId": "uuid", // dedup-ключ (идемпотентность)
"spacecraftId": "string",
"planId": "uuid", // план, по которому пришёл негатив (текущая база)
"occurredAt": "2026-06-02T10:00:00",
"reason": "string|null"
}
pcp.tgu.layin-failed.v1— «не заложен».pcp.tgu.plan-not-accepted.v1— «не принят».- Поведение пока идентично; обработчики раздельные.
Новый топик pcp.tgu.station-params-changed.v1, DTO StationParamsChangedMessage:
{
"eventId": "uuid", // dedup-ключ (идемпотентность)
"stationId": "string|null", // какая станция изменилась; пока не используется для сужения
"occurredAt": "2026-06-02T10:00:00",
"reason": "string|null"
}
- При получении — поставить
visibility_refresh_jobпо всем активным КА (перезапрос ЗРВ у баллистики), цепочка пересоберётся через on-demand проекцию.
9. Допущения и риски
- Источник «ближайшей ЗРВ» для срочной перевыдачи — тот же последний снапшот ЗРВ; если в нём нет окна со
startTime ≥ urgent_reissue_from, голова пуста — КА ждёт следующего refresh окон (это корректное поведение, не ошибка). - Идемпотентность новых событий — обязательна (at-least-once Kafka), через общий
processed_plan_decision_event. ddl-auto: validate: после правок прогнать миграции на чистой БД и убедиться, что Hibernate-валидация проходит.- Совместимость UI: JSON-форма
/api/plansне меняется; виртуальные планы отдаются с тем же набором полей иstatus: "PLANNED". - Сборка ресурсов: при правке
application.ymlпомнить про./gradlew :services:pcp-tgu-service:processResources(IntelliJ-рестарт не всегда пересобирает ресурсы).
10. Критерии приёмки (по ним проверяем)
10.1. Хранение «только выданное»
- При штатной работе (КА с окнами ЗРВ, ни одного дозревшего плана) в
planned_planнет ни одной строки для этого КА. Будущее видно только через/api/plansкакPLANNED. - Строка в
planned_planпоявляется ровно в момент выдачи и сразу в статусеISSUING(неPLANNED). - Статус
PLANNEDникогда не пишется в БД (проверяемо:SELECT count(*) FROM planned_plan WHERE status='PLANNED'= 0 в любой момент). - Выданные планы и их история не удаляются (ретеншн-джоба нет; нет
DELETE/SUPERSEDED-массовой чистки будущего). - В коде нет материализации цепочки:
upsertPlannedByBusinessKey,supersedeObsoleteFuturePlans,expireMissedPlansудалены.
10.2. Проекция будущего
GET /api/plansиGET /api/plans/{spacecraftId}возвращают persisted-планы плюс вычисленную проекцию будущего; JSON-контракт прежний, фронт работает без изменений.- Проекция совпадает по содержанию с тем, что раньше материализовалось (та же монотонная цепочка из
PlanCalculationService): для одних и тех же окон ЗРВ набор(start,end,kpp)будущих планов идентичен прежнему поведению. - Нет дублей: если бизнес-ключ есть в persisted, проекция его не добавляет повторно.
- Левый край истории по-прежнему регулируется
historyDays.
10.3. Снятие машинерии и мёртвого кода
- Таблиц
tracked_planиplan_recalculation_jobнет (миграцииDROPприменены); связанные сущности/репозитории/воркеры/сервисы удалены. SpacecraftVisibilitySnapshotServiceбольше не ставит recalc-job; сохранение снапшота и hash-проверка работают.- Проект компилируется без ссылок на удалённые классы; неиспользуемые методы репозитория убраны.
10.4. Закладочный флоу (оптимизм + реакция на негатив)
ACCEPTED→ план переходит вAWAITING_LAYIN, становится базой; цепочка продолжается;updateAcceptedPlanSlots-job ставится. Никакого ожидания подтверждения закладки в виде доп. строк.- Отсутствие любых событий после
ACCEPTED= «заложен» (план остаётся базой, ничего не делаем). - Событие
pcp.tgu.layin-failed.v1по базовому плану → план вLAYIN_FAILED, взводитсяurgent_reissue_from = now + reissue-prep-minutes, и ближайшая ЗРВ ≥ этого времени выдаётся немедленно (не дожидаясьissue_not_before). - Событие
pcp.tgu.plan-not-accepted.v1→ то же поведение, но статусNOT_ACCEPTED; обработчик отдельный (разные методы/консьюмеры), общее ядроreissueOnNearestWindow. - Оба события идемпотентны: повтор того же
eventId— no-op (повторной перевыдачи нет). reissue-prep-minutesберётся из конфига (по умолчанию 30); изменение значения меняет лид срочной выдачи.- Если в снапшоте нет ЗРВ ≥
urgent_reissue_from— выдачи нет, флаг сохраняется, повторная попытка произойдёт после обновления окон (без ошибок/исключений в логе).
10.4b. Реакция на изменение параметров станций
- Событие
pcp.tgu.station-params-changed.v1ставитvisibility_refresh_jobпо всем активным КА (reasonSTATION_PARAMS_CHANGED). - После перезапроса ЗРВ снапшоты обновляются, и
/api/plansотдаёт цепочку, пересобранную по новым окнам (без перезапуска сервиса и без ручного rebuild). - Событие идемпотентно по
eventId(повтор — без повторной постановки refresh). - Уже выданные/принятые планы ретроспективно не меняются; пересобирается только будущее.
10.5. Масштабирование / корректность под несколькими подами
- Claim во всех воркерах —
FOR UPDATE SKIP LOCKED; выдача плана и обработка решения — под lock на КА (один план не выдаётся дважды при 2–3 подах). - У одного КА одновременно не более одной активной попытки выдачи (
activeAttemptId). - Сервис стартует и работает с
external-points-lookahead-days: 10.
10.6. Конфиг и миграции
application.ymlиconfig-repo/pcp-tgu-service.yamlсодержат итоговые значения раздела 7 (lookahead 10,reissue-prep-minutes: 30, два новых топика).ddl-auto: validateпроходит на чистой БД после применения всех миграций (V7–V9).
10.7. Тесты и сборка
./gradlew :services:pcp-tgu-service:test— зелёный.- Есть тесты на: проекцию, on-demand выдачу, urgent-path,
ACCEPTED→AWAITING_LAYIN, оба новых консьюмера (идемпотентность + раздельность + общий reissue), read-объединение с дедупом. - Старые тесты снятой машинерии удалены, оставшиеся — зелёные.
11. Команды проверки
# из корня монорепо
./gradlew :services:pcp-tgu-service:test
./gradlew :services:pcp-tgu-service:processResources # после правок application.yml
# ручная проверка флоу (нужен tgu.test-controller.enabled=true локально):
# - запустить сервис на :7011, дождаться окон ЗРВ
# - GET http://localhost:7011/api/plans/{spacecraftId} -> в ответе PLANNED-проекция, в БД строк нет
# - сэмулировать ACCEPTED -> в БД ACCEPTED/AWAITING_LAYIN, проекция продолжается
# - отправить layin-failed/not-accepted -> терминальный статус + срочная выдача ближайшей ЗРВ