pcp-tgu-service: миграция с WebFlux на servlet/MVC

WebFlux в tgu был фиктивным: блокирующая JPA, контроллеры оборачивали
блокирующие вызовы в Mono.fromCallable{}.subscribeOn(boundedElastic()),
WebClient .block()-ался сразу. Единственный WebFlux-only сервис флота.

- build: подключён pcp-types-lib (тянет starter-web → servlet), springdoc
  webflux-ui → webmvc-ui; starter-webflux оставлен только ради WebClient
- контроллеры возвращают значения напрямую (без Mono-обёрток)
- удалено локальное Jackson-2 зеркало PcpTimeModule.kt → модуль из либы
  в CamundaJacksonConfig; Jackson-3 зеркало остаётся (Boot 4 держит
  Jackson 3 на HTTP-границе и для MVC), WebFluxJacksonConfig → RestJacksonConfig
This commit is contained in:
Дмитрий Соловьев
2026-06-02 23:14:12 +03:00
parent cde4ffa9b6
commit e68c17e792
9 changed files with 78 additions and 182 deletions
+27 -19
View File
@@ -65,17 +65,16 @@
`findAndRegisterModules()` (напр. `pcp-request-service`). `findAndRegisterModules()` (напр. `pcp-request-service`).
- Точечная регистрация в мапперах, что обходят оба: `pcp-dynamic-plan-service/JacksonConfig`. - Точечная регистрация в мапперах, что обходят оба: `pcp-dynamic-plan-service/JacksonConfig`.
**Особый случай — pcp-tgu-service.** Он не зависит от pcp-types-lib и работает на WebFlux **Особый случай — pcp-tgu-service.** Сервис мигрирован с WebFlux на **servlet/MVC** (см. ниже
(подключение либы притянуло бы `spring-boot-starter-web`/MVC и переключило тип приложения на «Миграция tgu на MVC») и теперь зависит от pcp-types-lib, как остальной флот. Но из-за Boot 4 на
сервлетный). Поэтому в tgu лежат **два локальных зеркала** (поведение обязано совпадать с HTTP-границе всё равно работает **Jackson 3**, поэтому время покрывается двумя путями:
библиотечным, правки синхронизировать), покрывающих обе Jackson-среды сервиса: - Jackson-2 модуль **из либы** (`pcp_types_lib.utils.json.PcpTimeModule`) — явно зарегистрирован в
- `config/PcpTimeModule.kt` (Jackson 2) — зарегистрирован в `CamundaJacksonConfig`, покрывает ручном маппере `CamundaJacksonConfig` (Camunda/внутренний парсинг); своего зеркала больше нет.
Camunda/внутренний парсинг; - `config/PcpTimeModuleV3.kt` (**Jackson 3**, `tools.jackson.*`) — REST HTTP message converters (MVC),
- `config/PcpTimeModuleV3.kt` (**Jackson 3**, `tools.jackson.*`) — REST-кодеки WebFlux, которые которые Boot 4 авто-конфигурирует на Jackson 3 `JsonMapper`. Регистрируется бином
Boot 4 авто-конфигурирует на Jackson 3 `JsonMapper`. Регистрируется бином `tools.jackson.databind.JacksonModule` (`RestJacksonConfig`); `JacksonAutoConfiguration` собирает
`tools.jackson.databind.JacksonModule` (`WebFluxJacksonConfig`); `JacksonAutoConfiguration` все такие бины в общий `JsonMapper` (`builder.addModules(...)`). Лежит локально, т.к. либа даёт
собирает все такие бины в общий `JsonMapper` (`builder.addModules(...)`). Тем самым входящий только Jackson-2 модуль; поведение обязано совпадать с библиотечным, правки синхронизировать.
REST-JSON tgu тоже читается толерантно к обоим форматам.
Контракт слоя закреплён `PcpTimeModuleTest` (pcp-types-lib): оба формата → один и тот же UTC-момент, Контракт слоя закреплён `PcpTimeModuleTest` (pcp-types-lib): оба формата → один и тот же UTC-момент,
плюс проверка авто-подхвата через SPI. плюс проверка авто-подхвата через SPI.
@@ -139,8 +138,8 @@ mission-planing) используют тот же дефолтный Jackson-к
синхронно; фронт `parseUtc` совместим. Контракт закреплён тестами выше. синхронно; фронт `parseUtc` совместим. Контракт закреплён тестами выше.
- **Слой толерантности (`PcpTimeModule`) — раскатан по флоту.** 12 сервисов на pcp-types-lib - **Слой толерантности (`PcpTimeModule`) — раскатан по флоту.** 12 сервисов на pcp-types-lib
получают его авто-конфигурацией/SPI; dynamic-plan — точечно; tgu — двумя локальными зеркалами получают его авто-конфигурацией/SPI; dynamic-plan — точечно; tgu — модулем из либы (Camunda,
(Jackson-2 для Camunda и Jackson-3 для REST-кодеков WebFlux). Это включатель: дальнейший флип Jackson 2) плюс локальное Jackson-3 зеркало для REST-границы MVC. Это включатель: дальнейший флип
продюсеров на `Z` больше не требует синхронного деплоя. продюсеров на `Z` больше не требует синхронного деплоя.
После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO
@@ -155,16 +154,25 @@ ui-service, ballistics, complex-mission, dynamic-plan, slots, coverage-scheme.
`…+00:00`. **Инстант не изменился** (тот же момент), но стенные часы теперь совпадают с UTC, поэтому `…+00:00`. **Инстант не изменился** (тот же момент), но стенные часы теперь совпадают с UTC, поэтому
потребитель, дропающий зону, получает корректное время. Закреплено `LocalDateTimeTimeZoneSerializerTest`. потребитель, дропающий зону, получает корректное время. Закреплено `LocalDateTimeTimeZoneSerializerTest`.
### Исправленные баги формата - **pcp-tgu-service REST-граница на Jackson 3** — теперь толерантна (см. 2bis). Добавлен Jackson-3
- **pcp-tgu-service REST-codec на Jackson 3** — теперь толерантен (см. 2bis). Добавлен Jackson-3
вариант модуля `config/PcpTimeModuleV3.kt` и его регистрация бином `JacksonModule` вариант модуля `config/PcpTimeModuleV3.kt` и его регистрация бином `JacksonModule`
(`config/WebFluxJacksonConfig.kt`); входящий REST-JSON WebFlux читается в обоих форматах (`config/RestJacksonConfig.kt`); входящий REST-JSON читается в обоих форматах (naive=UTC и `Z`).
(naive=UTC и `Z`). Закреплено `PcpTimeModuleV3Test`. Закреплено `PcpTimeModuleV3Test`.
### Миграция tgu на MVC
- **pcp-tgu-service: WebFlux → servlet/MVC.** Сервис не имел реального реактивного кода — JPA
блокирующая, контроллеры оборачивали блокирующие вызовы в `Mono.fromCallable{}.subscribeOn(
boundedElastic())`, WebClient-вызовы `.block()`-ались сразу. Это был единственный WebFlux-only
сервис флота. Переведён на MVC: подключён pcp-types-lib (тянет `starter-web` → тип приложения
servlet), `springdoc``webmvc-ui`, контроллеры возвращают значения напрямую (без `Mono`).
`starter-webflux` оставлен только ради WebClient. Побочный эффект для времени: убрано локальное
Jackson-2 зеркало (`config/PcpTimeModule.kt`) в пользу модуля из либы; Jackson-3 зеркало остаётся
(Boot 4 держит Jackson 3 на HTTP-границе и для MVC).
### Остаток (под-задачи) ### Остаток (под-задачи)
- Обе Jackson-среды tgu теперь толерантны; отдельных остатков по чтению времени нет. При миграции - Обе Jackson-среды tgu толерантны; отдельных остатков по чтению времени нет. При миграции
оставшихся продьюсеров на `Z` (раздел 4) добавлять сериализационные тесты контракта. оставшихся продьюсеров на `Z` (раздел 4) добавлять сериализационные тесты контракта.
## 5. Правило для нового кода ## 5. Правило для нового кода
+7 -1
View File
@@ -17,11 +17,17 @@ java {
dependencies { dependencies {
// Общие типы/слой толерантности времени; тянет spring-boot-starter-web (servlet/MVC),
// из-за чего приложение запускается как сервлетное (MVC), а не WebFlux.
implementation(project(":libs:pcp-types-lib"))
// Spring Boot // Spring Boot
// starter-webflux оставлен только ради WebClient (внешние HTTP-клиенты, см. integration/api);
// тип приложения — сервлетный (MVC), т.к. в classpath есть spring-webmvc из starter-web выше.
implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-actuator")
runtimeOnly("io.micrometer:micrometer-registry-prometheus") runtimeOnly("io.micrometer:micrometer-registry-prometheus")
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:3.0.0") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${property("versions.open-api")}")
implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-flyway") implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql") implementation("org.flywaydb:flyway-database-postgresql")
@@ -10,6 +10,7 @@ import io.camunda.client.api.JsonMapper as CamundaJsonMapper
import io.camunda.client.impl.CamundaObjectMapper import io.camunda.client.impl.CamundaObjectMapper
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule
@Configuration @Configuration
class CamundaJacksonConfig { class CamundaJacksonConfig {
@@ -25,8 +26,8 @@ class CamundaJacksonConfig {
.addModule(KotlinModule.Builder().build()) .addModule(KotlinModule.Builder().build())
.addModule(JavaTimeModule()) .addModule(JavaTimeModule())
// Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md. // Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md.
// Покрывает Jackson-2 путь (Camunda/внутренний парсинг). REST-codec на Jackson 3 // Модуль из pcp-types-lib; покрывает Jackson-2 путь (этот ручной маппер для Camunda).
// (WebFlux auto-config) покрыт близнецом PcpTimeModuleV3 (см. WebFluxJacksonConfig). // REST-границу (Jackson 3) покрывает PcpTimeModuleV3 (см. RestJacksonConfig).
.addModule(PcpTimeModule()) .addModule(PcpTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build() .build()
@@ -1,94 +0,0 @@
package space.nstart.pcp_tgu_service.config
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer
import java.time.Instant
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeParseException
/**
* Локальное зеркало слоя толерантности времени из libs/pcp-types-lib
* (`space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule`).
*
* Канонический источник — там; здесь копия, потому что pcp-tgu-service работает на WebFlux,
* а pcp-types-lib тянет `spring-boot-starter-web` (MVC/Tomcat) и переключил бы тип приложения
* на сервлетный. Поведение обязано совпадать с библиотечным; правки синхронизировать.
*
* Любой потребитель с этим модулем читает время в обоих форматах на проводе (naive=UTC и `Z`/офсет),
* нормализуя к UTC. Трогает только десериализацию. См. docs/standards/DATETIME_UTC.md.
*/
class PcpTimeModule : SimpleModule(NAME) {
init {
addDeserializer(LocalDateTime::class.java, TolerantLocalDateTimeDeserializer())
addDeserializer(OffsetDateTime::class.java, TolerantOffsetDateTimeDeserializer())
addDeserializer(Instant::class.java, TolerantInstantDeserializer())
}
companion object {
const val NAME = "PcpTimeModule"
}
}
private class TolerantLocalDateTimeDeserializer : JsonDeserializer<LocalDateTime>() {
private val default = LocalDateTimeDeserializer.INSTANCE
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime? {
if (p.currentToken() == JsonToken.VALUE_STRING) {
val text = p.text.trim()
if (text.isEmpty()) return null
try {
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime()
} catch (_: DateTimeParseException) {
// naive-строка без зоны — трактуем как UTC
}
}
return default.deserialize(p, ctxt)
}
}
private class TolerantOffsetDateTimeDeserializer : JsonDeserializer<OffsetDateTime>() {
@Suppress("UNCHECKED_CAST")
private val default = InstantDeserializer.OFFSET_DATE_TIME as JsonDeserializer<OffsetDateTime>
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): OffsetDateTime? {
if (p.currentToken() == JsonToken.VALUE_STRING) {
val text = p.text.trim()
if (text.isEmpty()) return null
try {
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC)
} catch (_: DateTimeParseException) {
return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC)
}
}
return default.deserialize(p, ctxt)
}
}
private class TolerantInstantDeserializer : JsonDeserializer<Instant>() {
@Suppress("UNCHECKED_CAST")
private val default = InstantDeserializer.INSTANT as JsonDeserializer<Instant>
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Instant? {
if (p.currentToken() == JsonToken.VALUE_STRING) {
val text = p.text.trim()
if (text.isEmpty()) return null
try {
return OffsetDateTime.parse(text).toInstant()
} catch (_: DateTimeParseException) {
return LocalDateTime.parse(text).toInstant(ZoneOffset.UTC)
}
}
return default.deserialize(p, ctxt)
}
}
@@ -16,11 +16,12 @@ import java.time.format.DateTimeParseException
/** /**
* Слой толерантности времени для **Jackson 3** (`tools.jackson.*`). * Слой толерантности времени для **Jackson 3** (`tools.jackson.*`).
* *
* Близнец [PcpTimeModule] (Jackson 2): то же поведение — читать время в обоих форматах на проводе * Близнец библиотечного `PcpTimeModule` (Jackson 2): то же поведение — читать время в обоих
* (naive=UTC и `Z`/офсет), нормализуя к UTC; трогает только десериализацию. Нужен отдельно, потому * форматах на проводе (naive=UTC и `Z`/офсет), нормализуя к UTC; трогает только десериализацию.
* что Boot 4 авто-конфигурирует Jackson 3 `JsonMapper` для REST-кодеков WebFlux, а он не видит * Нужен отдельно, потому что Boot 4 авто-конфигурирует Jackson 3 `JsonMapper` для REST HTTP message
* Jackson-2 модули. Регистрируется как бин `tools.jackson.databind.JacksonModule` * converters (MVC), а он не видит Jackson-2 модули. Регистрируется как бин
* ([WebFluxJacksonConfig]) — `JacksonAutoConfiguration` собирает такие бины в общий `JsonMapper`. * `tools.jackson.databind.JacksonModule` ([RestJacksonConfig]) — `JacksonAutoConfiguration` собирает
* такие бины в общий `JsonMapper`.
* *
* Канонический источник поведения — `space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule`; * Канонический источник поведения — `space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule`;
* правки синхронизировать. См. docs/standards/DATETIME_UTC.md. * правки синхронизировать. См. docs/standards/DATETIME_UTC.md.
@@ -6,16 +6,15 @@ import tools.jackson.databind.JacksonModule
/** /**
* Регистрирует слой толерантности времени ([PcpTimeModuleV3]) в авто-конфигурируемом Jackson 3 * Регистрирует слой толерантности времени ([PcpTimeModuleV3]) в авто-конфигурируемом Jackson 3
* `JsonMapper`, который Boot 4 использует для REST-кодеков WebFlux. * `JsonMapper`, который Boot 4 использует для REST HTTP message converters (MVC).
* *
* `JacksonAutoConfiguration` собирает все бины `tools.jackson.databind.JacksonModule` из контекста * `JacksonAutoConfiguration` собирает все бины `tools.jackson.databind.JacksonModule` из контекста
* и добавляет их в общий `JsonMapper` (`builder.addModules(...)`), поэтому достаточно объявить бин * и добавляет их в общий `JsonMapper` (`builder.addModules(...)`), поэтому достаточно объявить бин
* этого типа. Закрывает tgu-follow-up из docs/standards/DATETIME_UTC.md: входящий REST-JSON теперь * этого типа. Входящий REST-JSON читается толерантно к обоим форматам времени (naive=UTC и `Z`),
* читается толерантно к обоим форматам времени (naive=UTC и `Z`), как и Jackson-2 путь * как и Jackson-2 путь Camunda ([CamundaJacksonConfig]). См. docs/standards/DATETIME_UTC.md.
* ([CamundaJacksonConfig]).
*/ */
@Configuration @Configuration
class WebFluxJacksonConfig { class RestJacksonConfig {
@Bean @Bean
fun pcpTimeModuleV3(): JacksonModule = PcpTimeModuleV3() fun pcpTimeModuleV3(): JacksonModule = PcpTimeModuleV3()
@@ -6,8 +6,6 @@ import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import space.nstart.pcp_tgu_service.dto.PlanResponse import space.nstart.pcp_tgu_service.dto.PlanResponse
import space.nstart.pcp_tgu_service.service.PlanQueryService import space.nstart.pcp_tgu_service.service.PlanQueryService
@@ -19,18 +17,14 @@ class PlanController(
/** Возвращает планы по всем КА (persisted ∪ проекция будущего) с окном истории. */ /** Возвращает планы по всем КА (persisted ∪ проекция будущего) с окном истории. */
@GetMapping @GetMapping
fun getAllPlans(@RequestParam(defaultValue = "7") historyDays: Long): Mono<List<PlanResponse>> = fun getAllPlans(@RequestParam(defaultValue = "7") historyDays: Long): List<PlanResponse> =
Mono.fromCallable {
planQueryService.getAllPlans(historyDays) planQueryService.getAllPlans(historyDays)
}.subscribeOn(Schedulers.boundedElastic())
/** Возвращает цепочку планов для одного КА (persisted ∪ проекция будущего). */ /** Возвращает цепочку планов для одного КА (persisted ∪ проекция будущего). */
@GetMapping("/{spacecraftId}") @GetMapping("/{spacecraftId}")
fun getPlansBySpacecraftId( fun getPlansBySpacecraftId(
@PathVariable spacecraftId: String, @PathVariable spacecraftId: String,
@RequestParam(defaultValue = "7") historyDays: Long @RequestParam(defaultValue = "7") historyDays: Long
): Mono<List<PlanResponse>> = ): List<PlanResponse> =
Mono.fromCallable {
planQueryService.getPlansBySpacecraftId(spacecraftId, historyDays) planQueryService.getPlansBySpacecraftId(spacecraftId, historyDays)
}.subscribeOn(Schedulers.boundedElastic())
} }
@@ -10,8 +10,6 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
/** /**
* REST-контроллер для просмотра списка космических аппаратов. * REST-контроллер для просмотра списка космических аппаратов.
@@ -30,10 +28,8 @@ class PlatformController(
@Operation(summary = "Получить список космических аппаратов") @Operation(summary = "Получить список космических аппаратов")
fun getPlatforms( fun getPlatforms(
@RequestParam(required = false) status: String? @RequestParam(required = false) status: String?
): Mono<List<PlatformResponse>> = ): List<PlatformResponse> =
Mono.fromCallable {
platformService.loadPlatforms(status).map { platform -> platform.toResponse() } platformService.loadPlatforms(status).map { platform -> platform.toResponse() }
}.subscribeOn(Schedulers.boundedElastic())
private fun PlatformCatalogItem.toResponse(): PlatformResponse = private fun PlatformCatalogItem.toResponse(): PlatformResponse =
PlatformResponse( PlatformResponse(
@@ -9,8 +9,6 @@ import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import space.nstart.pcp_tgu_service.domain.PlanDecision import space.nstart.pcp_tgu_service.domain.PlanDecision
import space.nstart.pcp_tgu_service.dto.PlanDecisionMessage import space.nstart.pcp_tgu_service.dto.PlanDecisionMessage
import space.nstart.pcp_tgu_service.dto.PlanResponse import space.nstart.pcp_tgu_service.dto.PlanResponse
@@ -49,31 +47,25 @@ class TestTguController(
fun refreshVisibility( fun refreshVisibility(
@PathVariable spacecraftId: String, @PathVariable spacecraftId: String,
@RequestParam(required = false) noradId: Long? @RequestParam(required = false) noradId: Long?
): Mono<Void> = ) {
Mono.fromRunnable<Void> {
visibilityRefreshJobService.enqueue(spacecraftId, noradId, "TEST_CONTROLLER") visibilityRefreshJobService.enqueue(spacecraftId, noradId, "TEST_CONTROLLER")
}.subscribeOn(Schedulers.boundedElastic()) }
/** Возвращает вычисленную on-demand проекцию будущей цепочки КА (диагностика). */ /** Возвращает вычисленную on-demand проекцию будущей цепочки КА (диагностика). */
@GetMapping("/spacecraft/{spacecraftId}/projection") @GetMapping("/spacecraft/{spacecraftId}/projection")
fun getProjection(@PathVariable spacecraftId: String): Mono<List<PlanResponse>> = fun getProjection(@PathVariable spacecraftId: String): List<PlanResponse> =
Mono.fromCallable {
planProjectionService.project(spacecraftId).map(PlanResponse::projected) planProjectionService.project(spacecraftId).map(PlanResponse::projected)
}.subscribeOn(Schedulers.boundedElastic())
/** Принудительно выдаёт конкретный persisted plan через новую attempt. */ /** Принудительно выдаёт конкретный persisted plan через новую attempt. */
@PostMapping("/plans/{planId}/issue") @PostMapping("/plans/{planId}/issue")
fun issuePlan(@PathVariable planId: UUID): Mono<ProcessStartResponse> = fun issuePlan(@PathVariable planId: UUID): ProcessStartResponse =
Mono.fromCallable {
issueDuePlanWorker.issuePlanNow(planId) issueDuePlanWorker.issuePlanNow(planId)
}.subscribeOn(Schedulers.boundedElastic())
/** Инжектит decision event через тот же use case, что Kafka consumer. */ /** Инжектит decision event через тот же use case, что Kafka consumer. */
@PostMapping("/plan-decisions") @PostMapping("/plan-decisions")
fun handleDecision(@RequestBody message: PlanDecisionMessage): Mono<Void> = fun handleDecision(@RequestBody message: PlanDecisionMessage) {
Mono.fromRunnable<Void> {
handlePlanDecisionUseCase.handle(message) handlePlanDecisionUseCase.handle(message)
}.subscribeOn(Schedulers.boundedElastic()) }
/** Генерирует ACCEPTED/REJECTED decision event для active attempt указанного persisted plan. */ /** Генерирует ACCEPTED/REJECTED decision event для active attempt указанного persisted plan. */
@PostMapping("/plans/{planId}/decision") @PostMapping("/plans/{planId}/decision")
@@ -81,23 +73,20 @@ class TestTguController(
@PathVariable planId: UUID, @PathVariable planId: UUID,
@RequestParam decision: PlanDecision, @RequestParam decision: PlanDecision,
@RequestParam(required = false) reason: String? @RequestParam(required = false) reason: String?
): Mono<PlanDecisionMessage> = ): PlanDecisionMessage =
Mono.fromCallable {
testPlanDecisionService.generateDecision(planId, decision, reason) testPlanDecisionService.generateDecision(planId, decision, reason)
}.subscribeOn(Schedulers.boundedElastic())
/** Загружает cache аппаратов из файла с root-array payload НСИ вместо запроса в НСИ. */ /** Загружает cache аппаратов из файла с root-array payload НСИ вместо запроса в НСИ. */
@PostMapping("/platforms/load-from-file") @PostMapping("/platforms/load-from-file")
fun loadPlatformsFromFile(@RequestParam("path") filePath: String): Mono<TestPlatformsLoadResponse> = fun loadPlatformsFromFile(@RequestParam("path") filePath: String): TestPlatformsLoadResponse {
Mono.fromCallable {
val path = Path.of(filePath).toAbsolutePath().normalize() val path = Path.of(filePath).toAbsolutePath().normalize()
val platforms = platformService.loadPlatformsFromNsiPayload(Files.readString(path)) val platforms = platformService.loadPlatformsFromNsiPayload(Files.readString(path))
TestPlatformsLoadResponse( return TestPlatformsLoadResponse(
filePath = path.toString(), filePath = path.toString(),
loadedCount = platforms.size, loadedCount = platforms.size,
allowedCount = platformService.loadAllowedPlatforms().size allowedCount = platformService.loadAllowedPlatforms().size
) )
}.subscribeOn(Schedulers.boundedElastic()) }
/** Загружает raw payload окон видимости из баллистики для диагностики. */ /** Загружает raw payload окон видимости из баллистики для диагностики. */
@GetMapping("/external-points/{noradId}") @GetMapping("/external-points/{noradId}")
@@ -109,17 +98,13 @@ class TestTguController(
@RequestParam("time_stop") @RequestParam("time_stop")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
timeStop: LocalDateTime timeStop: LocalDateTime
): Mono<String> = ): String =
Mono.fromCallable {
externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop) externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop)
}.subscribeOn(Schedulers.boundedElastic())
/** Возвращает persisted plan для проверки результата ручных операций. */ /** Возвращает persisted plan для проверки результата ручных операций. */
@GetMapping("/plans/{planId}") @GetMapping("/plans/{planId}")
fun getPlan(@PathVariable planId: UUID): Mono<PlanResponse> = fun getPlan(@PathVariable planId: UUID): PlanResponse =
Mono.fromCallable {
PlanResponse.from(planQueryService.getByPlanId(planId)) PlanResponse.from(planQueryService.getByPlanId(planId))
}.subscribeOn(Schedulers.boundedElastic())
} }
data class TestPlatformsLoadResponse( data class TestPlatformsLoadResponse(