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