diff --git a/config-repo/pcp-stations-service.yaml b/config-repo/pcp-stations-service.yaml index ff70a10..21530ea 100644 --- a/config-repo/pcp-stations-service.yaml +++ b/config-repo/pcp-stations-service.yaml @@ -91,3 +91,11 @@ server: classifier: 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} diff --git a/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/PcpStationsServiceApplication.kt b/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/PcpStationsServiceApplication.kt index 4184c89..1fa8648 100644 --- a/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/PcpStationsServiceApplication.kt +++ b/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/PcpStationsServiceApplication.kt @@ -1,9 +1,11 @@ package space.nstart.pcp.pcp_stations_service import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication @SpringBootApplication +@ConfigurationPropertiesScan class PcpStationsServiceApplication fun main(args: Array) { diff --git a/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/configuration/ClassifierProperties.kt b/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/configuration/ClassifierProperties.kt new file mode 100644 index 0000000..172eb18 --- /dev/null +++ b/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/configuration/ClassifierProperties.kt @@ -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) +) diff --git a/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/integration/NsiStationClient.kt b/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/integration/NsiStationClient.kt index 313ae7e..0badc46 100644 --- a/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/integration/NsiStationClient.kt +++ b/services/pcp-stations-service/src/main/kotlin/space/nstart/pcp/pcp_stations_service/integration/NsiStationClient.kt @@ -3,11 +3,12 @@ package space.nstart.pcp.pcp_stations_service.integration import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value +import org.springframework.http.MediaType import org.springframework.stereotype.Component 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 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.StationDTO import java.time.Duration @@ -16,41 +17,78 @@ import java.util.UUID @Component class NsiStationClient( webClientBuilder: WebClient.Builder, - - @Value("\${classifier.stations-url:https://ordinis.k8s.265.nstart.cloud/api/v1/ground_station/records}") - private val stationsUrl: String + private val classifierProperties: ClassifierProperties, ) { private val log = LoggerFactory.getLogger(javaClass) private val webClient: WebClient = webClientBuilder.build() + private fun classifyResponseException( + ex: WebClientResponseException + ): NsiStationsException { + val statusCode = ex.statusCode.value() + + return if ( + statusCode == 408 || + statusCode == 429 || + statusCode in 500..599 + ) { + NsiStationsUnavailableException( + classifierUrl = classifierProperties.stationsUrl, + statusCode = statusCode, + cause = ex + ) + } else { + NsiStationsResponseException( + classifierUrl = classifierProperties.stationsUrl, + statusCode = statusCode, + cause = ex + ) + } + } + fun fetchStations(): List { - val records = try { + log.info( + "Fetching stations from NSI. url={}", + classifierProperties.stationsUrl + ) + + val response: List = try { webClient.get() - .uri(stationsUrl) + .uri(classifierProperties.stationsUrl) + .accept(MediaType.APPLICATION_JSON) .retrieve() .bodyToMono(Array::class.java) - .block(Duration.ofSeconds(15)) + .block(Duration.ofSeconds(10)) ?.toList() - ?: emptyList() + ?: throw NsiStationsUnavailableException( + classifierUrl = classifierProperties.stationsUrl, + statusCode = null, + cause = IllegalStateException("NSI response body is absent") + ) + } catch (ex: NsiStationsException) { + throw ex } catch (ex: WebClientResponseException) { - log.error( - "NSI stations request failed. url={}, status={}, body={}", - stationsUrl, - ex.statusCode, - ex.responseBodyAsString, - ex + 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 ) - throw NsiStationsUnavailableException("NSI stations request failed: $stationsUrl", ex) - } catch (ex: WebClientException) { - log.error("NSI stations request failed. url={}", stationsUrl, ex) - throw NsiStationsUnavailableException("NSI stations request failed: $stationsUrl", ex) } - log.info("Loaded {} station records from NSI. url={}", records.size, stationsUrl) - - return records.mapIndexed { index, record -> - record.toStationDTO(number = index + 1) + return response.mapIndexed { index, station -> + station.toStationDTO(number = index + 1) } } @@ -75,11 +113,29 @@ class NsiStationClient( } } -class NsiStationsUnavailableException( +sealed class NsiStationsException( message: String, - cause: Throwable + cause: Throwable? ) : 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) data class NsiStationRecord( val id: String? = null,