main request, mdm properties set

This commit is contained in:
Ivan I. Ovchinnikov
2026-07-14 20:11:52 +03:00
parent b37009a169
commit 10a080bd8c
4 changed files with 104 additions and 25 deletions
+8
View File
@@ -91,3 +91,11 @@ server:
classifier: classifier:
stations-url: ${CLASSIFIER_STATIONS_URL:https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records} stations-url: ${CLASSIFIER_STATIONS_URL:https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records}
# cache TTL
stations-cache-ttl: ${CLASSIFIER_STATIONS_CACHE_TTL:15m}
# MDM retries
stations-refresh-attempts: ${CLASSIFIER_STATIONS_REFRESH_ATTEMPTS:3}
# MDM retry delay
stations-refresh-retry-delay: ${CLASSIFIER_STATIONS_REFRESH_RETRY_DELAY:300ms}
# MDM failure cooldown
stations-refresh-failure-cooldown: ${CLASSIFIER_STATIONS_REFRESH_FAILURE_COOLDOWN:5s}
@@ -1,9 +1,11 @@
package space.nstart.pcp.pcp_stations_service package space.nstart.pcp.pcp_stations_service
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
@SpringBootApplication @SpringBootApplication
@ConfigurationPropertiesScan
class PcpStationsServiceApplication class PcpStationsServiceApplication
fun main(args: Array<String>) { fun main(args: Array<String>) {
@@ -0,0 +1,13 @@
package space.nstart.pcp.pcp_stations_service.configuration
import org.springframework.boot.context.properties.ConfigurationProperties
import java.time.Duration
@ConfigurationProperties(prefix = "classifier")
data class ClassifierProperties(
val stationsUrl: String = "https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records",
val stationsCacheTtl: Duration = Duration.ofMinutes(15),
val stationsRefreshAttempts: Int = 3,
val stationsRefreshRetryDelay: Duration = Duration.ofMillis(300),
val stationsRefreshFailureCooldown: Duration = Duration.ofSeconds(5)
)
@@ -3,11 +3,12 @@ package space.nstart.pcp.pcp_stations_service.integration
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.http.MediaType
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.client.WebClient import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientException import org.springframework.web.reactive.function.client.WebClientRequestException
import org.springframework.web.reactive.function.client.WebClientResponseException import org.springframework.web.reactive.function.client.WebClientResponseException
import space.nstart.pcp.pcp_stations_service.configuration.ClassifierProperties
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.PositionDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO import space.nstart.pcp.pcp_types_lib.dto.ballistics.StationDTO
import java.time.Duration import java.time.Duration
@@ -16,41 +17,78 @@ import java.util.UUID
@Component @Component
class NsiStationClient( class NsiStationClient(
webClientBuilder: WebClient.Builder, webClientBuilder: WebClient.Builder,
private val classifierProperties: ClassifierProperties,
@Value("\${classifier.stations-url:https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records}")
private val stationsUrl: String
) { ) {
private val log = LoggerFactory.getLogger(javaClass) private val log = LoggerFactory.getLogger(javaClass)
private val webClient: WebClient = webClientBuilder.build() private val webClient: WebClient = webClientBuilder.build()
fun fetchStations(): List<StationDTO> { private fun classifyResponseException(
val records = try { ex: WebClientResponseException
webClient.get() ): NsiStationsException {
.uri(stationsUrl) val statusCode = ex.statusCode.value()
.retrieve()
.bodyToMono(Array<NsiStationRecord>::class.java) return if (
.block(Duration.ofSeconds(15)) statusCode == 408 ||
?.toList() statusCode == 429 ||
?: emptyList() statusCode in 500..599
} catch (ex: WebClientResponseException) { ) {
log.error( NsiStationsUnavailableException(
"NSI stations request failed. url={}, status={}, body={}", classifierUrl = classifierProperties.stationsUrl,
stationsUrl, statusCode = statusCode,
ex.statusCode, cause = ex
ex.responseBodyAsString,
ex
) )
throw NsiStationsUnavailableException("NSI stations request failed: $stationsUrl", ex) } else {
} catch (ex: WebClientException) { NsiStationsResponseException(
log.error("NSI stations request failed. url={}", stationsUrl, ex) classifierUrl = classifierProperties.stationsUrl,
throw NsiStationsUnavailableException("NSI stations request failed: $stationsUrl", ex) statusCode = statusCode,
cause = ex
)
}
} }
log.info("Loaded {} station records from NSI. url={}", records.size, stationsUrl) fun fetchStations(): List<StationDTO> {
log.info(
"Fetching stations from NSI. url={}",
classifierProperties.stationsUrl
)
return records.mapIndexed { index, record -> val response: List<NsiStationRecord> = try {
record.toStationDTO(number = index + 1) webClient.get()
.uri(classifierProperties.stationsUrl)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Array<NsiStationRecord>::class.java)
.block(Duration.ofSeconds(10))
?.toList()
?: throw NsiStationsUnavailableException(
classifierUrl = classifierProperties.stationsUrl,
statusCode = null,
cause = IllegalStateException("NSI response body is absent")
)
} catch (ex: NsiStationsException) {
throw ex
} catch (ex: WebClientResponseException) {
throw classifyResponseException(ex)
} catch (ex: WebClientRequestException) {
throw NsiStationsUnavailableException(
classifierUrl = classifierProperties.stationsUrl,
statusCode = null,
cause = ex
)
} catch (ex: IllegalStateException) {
/*
* Сюда, в частности, попадает timeout от block(Duration).
*/
throw NsiStationsUnavailableException(
classifierUrl = classifierProperties.stationsUrl,
statusCode = null,
cause = ex
)
}
return response.mapIndexed { index, station ->
station.toStationDTO(number = index + 1)
} }
} }
@@ -75,11 +113,29 @@ class NsiStationClient(
} }
} }
class NsiStationsUnavailableException( sealed class NsiStationsException(
message: String, message: String,
cause: Throwable cause: Throwable?
) : RuntimeException(message, cause) ) : RuntimeException(message, cause)
class NsiStationsUnavailableException(
val classifierUrl: String,
val statusCode: Int?,
cause: Throwable?
) : NsiStationsException(
message = "NSI stations classifier is temporarily unavailable: $classifierUrl",
cause = cause
)
class NsiStationsResponseException(
val classifierUrl: String,
val statusCode: Int,
cause: Throwable?
) : NsiStationsException(
message = "NSI stations classifier returned HTTP $statusCode: $classifierUrl",
cause = cause
)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class NsiStationRecord( data class NsiStationRecord(
val id: String? = null, val id: String? = null,