diff --git a/docs/standards/DATETIME_UTC.md b/docs/standards/DATETIME_UTC.md index c457f42..de1aaac 100644 --- a/docs/standards/DATETIME_UTC.md +++ b/docs/standards/DATETIME_UTC.md @@ -65,17 +65,16 @@ `findAndRegisterModules()` (напр. `pcp-request-service`). - Точечная регистрация в мапперах, что обходят оба: `pcp-dynamic-plan-service/JacksonConfig`. -**Особый случай — pcp-tgu-service.** Он не зависит от pcp-types-lib и работает на WebFlux -(подключение либы притянуло бы `spring-boot-starter-web`/MVC и переключило тип приложения на -сервлетный). Поэтому в tgu лежат **два локальных зеркала** (поведение обязано совпадать с -библиотечным, правки синхронизировать), покрывающих обе Jackson-среды сервиса: -- `config/PcpTimeModule.kt` (Jackson 2) — зарегистрирован в `CamundaJacksonConfig`, покрывает - Camunda/внутренний парсинг; -- `config/PcpTimeModuleV3.kt` (**Jackson 3**, `tools.jackson.*`) — REST-кодеки WebFlux, которые - Boot 4 авто-конфигурирует на Jackson 3 `JsonMapper`. Регистрируется бином - `tools.jackson.databind.JacksonModule` (`WebFluxJacksonConfig`); `JacksonAutoConfiguration` - собирает все такие бины в общий `JsonMapper` (`builder.addModules(...)`). Тем самым входящий - REST-JSON tgu тоже читается толерантно к обоим форматам. +**Особый случай — pcp-tgu-service.** Сервис мигрирован с WebFlux на **servlet/MVC** (см. ниже +«Миграция tgu на MVC») и теперь зависит от pcp-types-lib, как остальной флот. Но из-за Boot 4 на +HTTP-границе всё равно работает **Jackson 3**, поэтому время покрывается двумя путями: +- Jackson-2 модуль **из либы** (`pcp_types_lib.utils.json.PcpTimeModule`) — явно зарегистрирован в + ручном маппере `CamundaJacksonConfig` (Camunda/внутренний парсинг); своего зеркала больше нет. +- `config/PcpTimeModuleV3.kt` (**Jackson 3**, `tools.jackson.*`) — REST HTTP message converters (MVC), + которые Boot 4 авто-конфигурирует на Jackson 3 `JsonMapper`. Регистрируется бином + `tools.jackson.databind.JacksonModule` (`RestJacksonConfig`); `JacksonAutoConfiguration` собирает + все такие бины в общий `JsonMapper` (`builder.addModules(...)`). Лежит локально, т.к. либа даёт + только Jackson-2 модуль; поведение обязано совпадать с библиотечным, правки синхронизировать. Контракт слоя закреплён `PcpTimeModuleTest` (pcp-types-lib): оба формата → один и тот же UTC-момент, плюс проверка авто-подхвата через SPI. @@ -139,8 +138,8 @@ mission-planing) используют тот же дефолтный Jackson-к синхронно; фронт `parseUtc` совместим. Контракт закреплён тестами выше. - **Слой толерантности (`PcpTimeModule`) — раскатан по флоту.** 12 сервисов на pcp-types-lib - получают его авто-конфигурацией/SPI; dynamic-plan — точечно; tgu — двумя локальными зеркалами - (Jackson-2 для Camunda и Jackson-3 для REST-кодеков WebFlux). Это включатель: дальнейший флип + получают его авто-конфигурацией/SPI; dynamic-plan — точечно; tgu — модулем из либы (Camunda, + Jackson 2) плюс локальное Jackson-3 зеркало для REST-границы MVC. Это включатель: дальнейший флип продюсеров на `Z` больше не требует синхронного деплоя. После толерантности продюсеров можно флипать **в любом порядке**. Кандидаты: остальные naive-DTO @@ -155,16 +154,25 @@ ui-service, ballistics, complex-mission, dynamic-plan, slots, coverage-scheme. `…+00:00`. **Инстант не изменился** (тот же момент), но стенные часы теперь совпадают с UTC, поэтому потребитель, дропающий зону, получает корректное время. Закреплено `LocalDateTimeTimeZoneSerializerTest`. -### Исправленные баги формата - -- **pcp-tgu-service REST-codec на Jackson 3** — теперь толерантен (см. 2bis). Добавлен Jackson-3 +- **pcp-tgu-service REST-граница на Jackson 3** — теперь толерантна (см. 2bis). Добавлен Jackson-3 вариант модуля `config/PcpTimeModuleV3.kt` и его регистрация бином `JacksonModule` - (`config/WebFluxJacksonConfig.kt`); входящий REST-JSON WebFlux читается в обоих форматах - (naive=UTC и `Z`). Закреплено `PcpTimeModuleV3Test`. + (`config/RestJacksonConfig.kt`); входящий REST-JSON читается в обоих форматах (naive=UTC и `Z`). + Закреплено `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) добавлять сериализационные тесты контракта. ## 5. Правило для нового кода diff --git a/services/pcp-tgu-service/build.gradle.kts b/services/pcp-tgu-service/build.gradle.kts index 05636e5..733dc7b 100644 --- a/services/pcp-tgu-service/build.gradle.kts +++ b/services/pcp-tgu-service/build.gradle.kts @@ -17,11 +17,17 @@ java { dependencies { + // Общие типы/слой толерантности времени; тянет spring-boot-starter-web (servlet/MVC), + // из-за чего приложение запускается как сервлетное (MVC), а не WebFlux. + implementation(project(":libs:pcp-types-lib")) + // 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-actuator") 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-flyway") implementation("org.flywaydb:flyway-database-postgresql") diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt index fb8da9c..0c04f21 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/CamundaJacksonConfig.kt @@ -10,6 +10,7 @@ import io.camunda.client.api.JsonMapper as CamundaJsonMapper import io.camunda.client.impl.CamundaObjectMapper import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule @Configuration class CamundaJacksonConfig { @@ -25,8 +26,8 @@ class CamundaJacksonConfig { .addModule(KotlinModule.Builder().build()) .addModule(JavaTimeModule()) // Слой толерантности времени (читать naive=UTC и Z) — см. docs/standards/DATETIME_UTC.md. - // Покрывает Jackson-2 путь (Camunda/внутренний парсинг). REST-codec на Jackson 3 - // (WebFlux auto-config) покрыт близнецом PcpTimeModuleV3 (см. WebFluxJacksonConfig). + // Модуль из pcp-types-lib; покрывает Jackson-2 путь (этот ручной маппер для Camunda). + // REST-границу (Jackson 3) покрывает PcpTimeModuleV3 (см. RestJacksonConfig). .addModule(PcpTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .build() diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModule.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModule.kt deleted file mode 100644 index e359d09..0000000 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModule.kt +++ /dev/null @@ -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() { - - 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() { - - @Suppress("UNCHECKED_CAST") - private val default = InstantDeserializer.OFFSET_DATE_TIME as JsonDeserializer - - 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() { - - @Suppress("UNCHECKED_CAST") - private val default = InstantDeserializer.INSTANT as JsonDeserializer - - 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) - } -} diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3.kt index 316b836..bed6d43 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/PcpTimeModuleV3.kt @@ -16,11 +16,12 @@ import java.time.format.DateTimeParseException /** * Слой толерантности времени для **Jackson 3** (`tools.jackson.*`). * - * Близнец [PcpTimeModule] (Jackson 2): то же поведение — читать время в обоих форматах на проводе - * (naive=UTC и `Z`/офсет), нормализуя к UTC; трогает только десериализацию. Нужен отдельно, потому - * что Boot 4 авто-конфигурирует Jackson 3 `JsonMapper` для REST-кодеков WebFlux, а он не видит - * Jackson-2 модули. Регистрируется как бин `tools.jackson.databind.JacksonModule` - * ([WebFluxJacksonConfig]) — `JacksonAutoConfiguration` собирает такие бины в общий `JsonMapper`. + * Близнец библиотечного `PcpTimeModule` (Jackson 2): то же поведение — читать время в обоих + * форматах на проводе (naive=UTC и `Z`/офсет), нормализуя к UTC; трогает только десериализацию. + * Нужен отдельно, потому что Boot 4 авто-конфигурирует Jackson 3 `JsonMapper` для REST HTTP message + * converters (MVC), а он не видит Jackson-2 модули. Регистрируется как бин + * `tools.jackson.databind.JacksonModule` ([RestJacksonConfig]) — `JacksonAutoConfiguration` собирает + * такие бины в общий `JsonMapper`. * * Канонический источник поведения — `space.nstart.pcp.pcp_types_lib.utils.json.PcpTimeModule`; * правки синхронизировать. См. docs/standards/DATETIME_UTC.md. diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/WebFluxJacksonConfig.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/RestJacksonConfig.kt similarity index 68% rename from services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/WebFluxJacksonConfig.kt rename to services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/RestJacksonConfig.kt index 343f848..4dcb2e9 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/WebFluxJacksonConfig.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/config/RestJacksonConfig.kt @@ -6,16 +6,15 @@ import tools.jackson.databind.JacksonModule /** * Регистрирует слой толерантности времени ([PcpTimeModuleV3]) в авто-конфигурируемом Jackson 3 - * `JsonMapper`, который Boot 4 использует для REST-кодеков WebFlux. + * `JsonMapper`, который Boot 4 использует для REST HTTP message converters (MVC). * * `JacksonAutoConfiguration` собирает все бины `tools.jackson.databind.JacksonModule` из контекста * и добавляет их в общий `JsonMapper` (`builder.addModules(...)`), поэтому достаточно объявить бин - * этого типа. Закрывает tgu-follow-up из docs/standards/DATETIME_UTC.md: входящий REST-JSON теперь - * читается толерантно к обоим форматам времени (naive=UTC и `Z`), как и Jackson-2 путь - * ([CamundaJacksonConfig]). + * этого типа. Входящий REST-JSON читается толерантно к обоим форматам времени (naive=UTC и `Z`), + * как и Jackson-2 путь Camunda ([CamundaJacksonConfig]). См. docs/standards/DATETIME_UTC.md. */ @Configuration -class WebFluxJacksonConfig { +class RestJacksonConfig { @Bean fun pcpTimeModuleV3(): JacksonModule = PcpTimeModuleV3() diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt index 96da5e6..b67a0bb 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlanController.kt @@ -6,8 +6,6 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam 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.service.PlanQueryService @@ -19,18 +17,14 @@ class PlanController( /** Возвращает планы по всем КА (persisted ∪ проекция будущего) с окном истории. */ @GetMapping - fun getAllPlans(@RequestParam(defaultValue = "7") historyDays: Long): Mono> = - Mono.fromCallable { - planQueryService.getAllPlans(historyDays) - }.subscribeOn(Schedulers.boundedElastic()) + fun getAllPlans(@RequestParam(defaultValue = "7") historyDays: Long): List = + planQueryService.getAllPlans(historyDays) /** Возвращает цепочку планов для одного КА (persisted ∪ проекция будущего). */ @GetMapping("/{spacecraftId}") fun getPlansBySpacecraftId( @PathVariable spacecraftId: String, @RequestParam(defaultValue = "7") historyDays: Long - ): Mono> = - Mono.fromCallable { - planQueryService.getPlansBySpacecraftId(spacecraftId, historyDays) - }.subscribeOn(Schedulers.boundedElastic()) + ): List = + planQueryService.getPlansBySpacecraftId(spacecraftId, historyDays) } diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlatformController.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlatformController.kt index 0d17c5d..4cd59bd 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlatformController.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/PlatformController.kt @@ -10,8 +10,6 @@ import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController -import reactor.core.publisher.Mono -import reactor.core.scheduler.Schedulers /** * REST-контроллер для просмотра списка космических аппаратов. @@ -30,10 +28,8 @@ class PlatformController( @Operation(summary = "Получить список космических аппаратов") fun getPlatforms( @RequestParam(required = false) status: String? - ): Mono> = - Mono.fromCallable { - platformService.loadPlatforms(status).map { platform -> platform.toResponse() } - }.subscribeOn(Schedulers.boundedElastic()) + ): List = + platformService.loadPlatforms(status).map { platform -> platform.toResponse() } private fun PlatformCatalogItem.toResponse(): PlatformResponse = PlatformResponse( diff --git a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt index 7f63d1f..be17d65 100644 --- a/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt +++ b/services/pcp-tgu-service/src/main/kotlin/space/nstart/pcp_tgu_service/controller/TestTguController.kt @@ -9,8 +9,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam 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.dto.PlanDecisionMessage import space.nstart.pcp_tgu_service.dto.PlanResponse @@ -49,31 +47,25 @@ class TestTguController( fun refreshVisibility( @PathVariable spacecraftId: String, @RequestParam(required = false) noradId: Long? - ): Mono = - Mono.fromRunnable { - visibilityRefreshJobService.enqueue(spacecraftId, noradId, "TEST_CONTROLLER") - }.subscribeOn(Schedulers.boundedElastic()) + ) { + visibilityRefreshJobService.enqueue(spacecraftId, noradId, "TEST_CONTROLLER") + } /** Возвращает вычисленную on-demand проекцию будущей цепочки КА (диагностика). */ @GetMapping("/spacecraft/{spacecraftId}/projection") - fun getProjection(@PathVariable spacecraftId: String): Mono> = - Mono.fromCallable { - planProjectionService.project(spacecraftId).map(PlanResponse::projected) - }.subscribeOn(Schedulers.boundedElastic()) + fun getProjection(@PathVariable spacecraftId: String): List = + planProjectionService.project(spacecraftId).map(PlanResponse::projected) /** Принудительно выдаёт конкретный persisted plan через новую attempt. */ @PostMapping("/plans/{planId}/issue") - fun issuePlan(@PathVariable planId: UUID): Mono = - Mono.fromCallable { - issueDuePlanWorker.issuePlanNow(planId) - }.subscribeOn(Schedulers.boundedElastic()) + fun issuePlan(@PathVariable planId: UUID): ProcessStartResponse = + issueDuePlanWorker.issuePlanNow(planId) /** Инжектит decision event через тот же use case, что Kafka consumer. */ @PostMapping("/plan-decisions") - fun handleDecision(@RequestBody message: PlanDecisionMessage): Mono = - Mono.fromRunnable { - handlePlanDecisionUseCase.handle(message) - }.subscribeOn(Schedulers.boundedElastic()) + fun handleDecision(@RequestBody message: PlanDecisionMessage) { + handlePlanDecisionUseCase.handle(message) + } /** Генерирует ACCEPTED/REJECTED decision event для active attempt указанного persisted plan. */ @PostMapping("/plans/{planId}/decision") @@ -81,23 +73,20 @@ class TestTguController( @PathVariable planId: UUID, @RequestParam decision: PlanDecision, @RequestParam(required = false) reason: String? - ): Mono = - Mono.fromCallable { - testPlanDecisionService.generateDecision(planId, decision, reason) - }.subscribeOn(Schedulers.boundedElastic()) + ): PlanDecisionMessage = + testPlanDecisionService.generateDecision(planId, decision, reason) /** Загружает cache аппаратов из файла с root-array payload НСИ вместо запроса в НСИ. */ @PostMapping("/platforms/load-from-file") - fun loadPlatformsFromFile(@RequestParam("path") filePath: String): Mono = - Mono.fromCallable { - val path = Path.of(filePath).toAbsolutePath().normalize() - val platforms = platformService.loadPlatformsFromNsiPayload(Files.readString(path)) - TestPlatformsLoadResponse( - filePath = path.toString(), - loadedCount = platforms.size, - allowedCount = platformService.loadAllowedPlatforms().size - ) - }.subscribeOn(Schedulers.boundedElastic()) + fun loadPlatformsFromFile(@RequestParam("path") filePath: String): TestPlatformsLoadResponse { + val path = Path.of(filePath).toAbsolutePath().normalize() + val platforms = platformService.loadPlatformsFromNsiPayload(Files.readString(path)) + return TestPlatformsLoadResponse( + filePath = path.toString(), + loadedCount = platforms.size, + allowedCount = platformService.loadAllowedPlatforms().size + ) + } /** Загружает raw payload окон видимости из баллистики для диагностики. */ @GetMapping("/external-points/{noradId}") @@ -109,17 +98,13 @@ class TestTguController( @RequestParam("time_stop") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) timeStop: LocalDateTime - ): Mono = - Mono.fromCallable { - externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop) - }.subscribeOn(Schedulers.boundedElastic()) + ): String = + externalPointsClient.fetchRawVisibilityPayload(noradId, timeStart, timeStop) /** Возвращает persisted plan для проверки результата ручных операций. */ @GetMapping("/plans/{planId}") - fun getPlan(@PathVariable planId: UUID): Mono = - Mono.fromCallable { - PlanResponse.from(planQueryService.getByPlanId(planId)) - }.subscribeOn(Schedulers.boundedElastic()) + fun getPlan(@PathVariable planId: UUID): PlanResponse = + PlanResponse.from(planQueryService.getByPlanId(planId)) } data class TestPlatformsLoadResponse(