Добавлен кэш, запросы в НСИ раз в 15 минут
This commit is contained in:
@@ -66,6 +66,7 @@ planning:
|
||||
|
||||
classifier:
|
||||
platforms-url: https://ordinis.nstart.cloud/api/v1/spacecraft/records
|
||||
platforms-cache-ttl: 15m
|
||||
allowed-platform-statuses:
|
||||
- OPERATIONAL
|
||||
- STANDBY
|
||||
|
||||
@@ -39,6 +39,7 @@ topics:
|
||||
|
||||
classifier:
|
||||
platforms-url: https://ordinis.nstart.cloud/api/v1/spacecraft/records
|
||||
platforms-cache-ttl: 15m
|
||||
allowed-platform-statuses:
|
||||
- OPERATIONAL
|
||||
- STANDBY
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
package space.nstart.pcp_tgu_service.config
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Настройки внешних классификаторов.
|
||||
@@ -11,6 +12,9 @@ data class ClassifierProperties(
|
||||
/** URL классификатора космических аппаратов. */
|
||||
val platformsUrl: String,
|
||||
|
||||
/** TTL in-memory cache списка платформ НСИ. */
|
||||
val platformsCacheTtl: Duration,
|
||||
|
||||
/** Статусы КА из НСИ, с которыми сервису разрешено работать. */
|
||||
val allowedPlatformStatuses: Set<String>
|
||||
)
|
||||
|
||||
+40
-3
@@ -7,19 +7,24 @@ import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
|
||||
import space.nstart.pcp_tgu_service.integration.api.PlatformsClassifierClient
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
|
||||
@Service
|
||||
class PlatformService(
|
||||
private val platformsClassifierClient: PlatformsClassifierClient,
|
||||
private val classifierProperties: ClassifierProperties
|
||||
private val classifierProperties: ClassifierProperties,
|
||||
private val clock: Clock = Clock.systemUTC()
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
private val cacheLock = Any()
|
||||
|
||||
@Volatile
|
||||
private var cachedPlatforms: PlatformsCache? = null
|
||||
|
||||
/** Возвращает платформы из классификатора с необязательной фильтрацией по статусу НСИ. */
|
||||
fun loadPlatforms(status: String? = null): List<PlatformCatalogItem> {
|
||||
val platforms = platformsClassifierClient.fetchPlatforms()
|
||||
.mapNotNull { platform -> platform.toCatalogItem() }
|
||||
val platforms = loadCachedPlatforms()
|
||||
|
||||
return platforms.filter { platform -> status.isNullOrBlank() || platform.status == status }
|
||||
}
|
||||
@@ -35,6 +40,31 @@ class PlatformService(
|
||||
now.isBefore(platform.validTo)
|
||||
}
|
||||
|
||||
private fun loadCachedPlatforms(): List<PlatformCatalogItem> {
|
||||
val now = Instant.now(clock)
|
||||
cachedPlatforms?.takeIf { cache -> cache.isActual(now) }?.let { cache -> return cache.platforms }
|
||||
|
||||
return synchronized(cacheLock) {
|
||||
val currentNow = Instant.now(clock)
|
||||
cachedPlatforms
|
||||
?.takeIf { cache -> cache.isActual(currentNow) }
|
||||
?.platforms
|
||||
?: fetchAndCachePlatforms(currentNow)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAndCachePlatforms(now: Instant): List<PlatformCatalogItem> {
|
||||
val platforms = platformsClassifierClient.fetchPlatforms()
|
||||
.mapNotNull { platform -> platform.toCatalogItem() }
|
||||
|
||||
cachedPlatforms = PlatformsCache(
|
||||
platforms = platforms,
|
||||
expiresAt = now.plus(classifierProperties.platformsCacheTtl)
|
||||
)
|
||||
|
||||
return platforms
|
||||
}
|
||||
|
||||
private fun NsiPlatformDto.toCatalogItem(): PlatformCatalogItem? {
|
||||
val nsiId = id
|
||||
if (nsiId == null) {
|
||||
@@ -53,4 +83,11 @@ class PlatformService(
|
||||
validTo = validTo
|
||||
)
|
||||
}
|
||||
|
||||
private data class PlatformsCache(
|
||||
val platforms: List<PlatformCatalogItem>,
|
||||
val expiresAt: Instant
|
||||
) {
|
||||
fun isActual(now: Instant): Boolean = now.isBefore(expiresAt)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -53,6 +53,7 @@ class ExternalPointsClientTest {
|
||||
mock(PlatformsClassifierClient::class.java),
|
||||
ClassifierProperties(
|
||||
platformsUrl = "http://localhost",
|
||||
platformsCacheTtl = java.time.Duration.ofMinutes(15),
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
) {
|
||||
|
||||
+3
@@ -11,6 +11,7 @@ import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
|
||||
import java.net.InetSocketAddress
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.time.Duration
|
||||
|
||||
class PlatformsClassifierClientTest {
|
||||
|
||||
@@ -37,6 +38,7 @@ class PlatformsClassifierClientTest {
|
||||
val client = PlatformsClassifierClient(
|
||||
ClassifierProperties(
|
||||
platformsUrl = baseUrl,
|
||||
platformsCacheTtl = Duration.ofMinutes(15),
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
)
|
||||
@@ -59,6 +61,7 @@ class PlatformsClassifierClientTest {
|
||||
val client = PlatformsClassifierClient(
|
||||
ClassifierProperties(
|
||||
platformsUrl = baseUrl,
|
||||
platformsCacheTtl = Duration.ofMinutes(15),
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
)
|
||||
|
||||
+84
-6
@@ -3,12 +3,18 @@ package space.nstart.pcp_tgu_service.service
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.times
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.`when`
|
||||
import space.nstart.pcp_tgu_service.config.ClassifierProperties
|
||||
import space.nstart.pcp_tgu_service.dto.NsiPlatformDataDto
|
||||
import space.nstart.pcp_tgu_service.dto.NsiPlatformDto
|
||||
import space.nstart.pcp_tgu_service.integration.api.PlatformsClassifierClient
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class PlatformServiceTest {
|
||||
|
||||
@@ -38,15 +44,64 @@ class PlatformServiceTest {
|
||||
assertEquals("MISSION", platforms.single().mission)
|
||||
assertEquals(Instant.parse("2026-01-01T00:00:00Z"), platforms.single().validFrom)
|
||||
assertEquals(Instant.parse("2026-12-31T00:00:00Z"), platforms.single().validTo)
|
||||
verify(client, times(1)).fetchPlatforms()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadAllowedPlatforms uses configured statuses noradId and validity interval`() {
|
||||
fun `first loadPlatforms call fetches platforms from client`() {
|
||||
val client = mock(PlatformsClassifierClient::class.java)
|
||||
`when`(client.fetchPlatforms()).thenReturn(
|
||||
listOf(platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 1L))
|
||||
)
|
||||
val service = service(client)
|
||||
|
||||
val platforms = service.loadPlatforms()
|
||||
|
||||
assertEquals(listOf("operational-id"), platforms.map { it.nsiId })
|
||||
verify(client, times(1)).fetchPlatforms()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `second loadPlatforms call within ttl uses cached platforms`() {
|
||||
val client = mock(PlatformsClassifierClient::class.java)
|
||||
`when`(client.fetchPlatforms()).thenReturn(
|
||||
listOf(platformDto(id = "cached-id", status = "OPERATIONAL", noradId = 1L))
|
||||
)
|
||||
val service = service(client)
|
||||
|
||||
val firstCall = service.loadPlatforms()
|
||||
val secondCall = service.loadPlatforms()
|
||||
|
||||
assertEquals(listOf("cached-id"), firstCall.map { it.nsiId })
|
||||
assertEquals(listOf("cached-id"), secondCall.map { it.nsiId })
|
||||
verify(client, times(1)).fetchPlatforms()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadPlatforms refreshes cache after fifteen minutes`() {
|
||||
val clock = MutableClock(Instant.parse("2026-05-26T12:00:00Z"))
|
||||
val client = mock(PlatformsClassifierClient::class.java)
|
||||
`when`(client.fetchPlatforms())
|
||||
.thenReturn(listOf(platformDto(id = "first-id", status = "OPERATIONAL", noradId = 1L)))
|
||||
.thenReturn(listOf(platformDto(id = "refreshed-id", status = "OPERATIONAL", noradId = 2L)))
|
||||
val service = service(client, clock)
|
||||
|
||||
val firstCall = service.loadPlatforms()
|
||||
clock.advance(Duration.ofMinutes(15))
|
||||
val refreshedCall = service.loadPlatforms()
|
||||
|
||||
assertEquals(listOf("first-id"), firstCall.map { it.nsiId })
|
||||
assertEquals(listOf("refreshed-id"), refreshedCall.map { it.nsiId })
|
||||
verify(client, times(2)).fetchPlatforms()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadAllowedPlatforms uses cached platforms and configured filtering`() {
|
||||
val now = Instant.parse("2026-05-26T12:00:00Z")
|
||||
val client = mock(PlatformsClassifierClient::class.java)
|
||||
`when`(client.fetchPlatforms()).thenReturn(
|
||||
listOf(
|
||||
platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 1L),
|
||||
platformDto(id = "operational-id", status = "OPERATIONAL", noradId = 1L, mission = null),
|
||||
platformDto(id = "standby-id", status = "STANDBY", noradId = 2L),
|
||||
platformDto(id = "lost-id", status = "LOST", noradId = 3L),
|
||||
platformDto(id = "without-norad-id", status = "OPERATIONAL", noradId = null),
|
||||
@@ -58,18 +113,26 @@ class PlatformServiceTest {
|
||||
)
|
||||
val service = service(client)
|
||||
|
||||
val cachedPlatforms = service.loadPlatforms()
|
||||
val allowedPlatforms = service.loadAllowedPlatforms(now)
|
||||
|
||||
assertEquals(8, cachedPlatforms.size)
|
||||
assertEquals(listOf("operational-id", "standby-id"), allowedPlatforms.map { it.nsiId })
|
||||
verify(client, times(1)).fetchPlatforms()
|
||||
}
|
||||
|
||||
private fun service(client: PlatformsClassifierClient): PlatformService =
|
||||
private fun service(
|
||||
client: PlatformsClassifierClient,
|
||||
clock: Clock = Clock.fixed(Instant.parse("2026-05-26T12:00:00Z"), ZoneOffset.UTC)
|
||||
): PlatformService =
|
||||
PlatformService(
|
||||
platformsClassifierClient = client,
|
||||
classifierProperties = ClassifierProperties(
|
||||
platformsUrl = "http://localhost",
|
||||
platformsCacheTtl = Duration.ofMinutes(15),
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
),
|
||||
clock = clock
|
||||
)
|
||||
|
||||
private fun platformDto(
|
||||
@@ -77,7 +140,8 @@ class PlatformServiceTest {
|
||||
status: String,
|
||||
noradId: Long?,
|
||||
validFrom: String? = "2026-01-01T00:00:00Z",
|
||||
validTo: String? = "2026-12-31T00:00:00Z"
|
||||
validTo: String? = "2026-12-31T00:00:00Z",
|
||||
mission: String? = "MISSION"
|
||||
): NsiPlatformDto =
|
||||
NsiPlatformDto(
|
||||
id = id,
|
||||
@@ -86,9 +150,23 @@ class PlatformServiceTest {
|
||||
name = id?.let { "Satellite $it" },
|
||||
status = status,
|
||||
noradId = noradId,
|
||||
mission = "MISSION"
|
||||
mission = mission
|
||||
),
|
||||
validFrom = validFrom?.let(Instant::parse),
|
||||
validTo = validTo?.let(Instant::parse)
|
||||
)
|
||||
|
||||
private class MutableClock(
|
||||
private var currentInstant: Instant
|
||||
) : Clock() {
|
||||
override fun getZone(): ZoneId = ZoneOffset.UTC
|
||||
|
||||
override fun withZone(zone: ZoneId): Clock = this
|
||||
|
||||
override fun instant(): Instant = currentInstant
|
||||
|
||||
fun advance(duration: Duration) {
|
||||
currentInstant = currentInstant.plus(duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -226,6 +226,7 @@ class SatelliteMissionProcessServiceTest {
|
||||
mock(PlatformsClassifierClient::class.java),
|
||||
ClassifierProperties(
|
||||
platformsUrl = "http://localhost",
|
||||
platformsCacheTtl = java.time.Duration.ofMinutes(15),
|
||||
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
|
||||
)
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user