Добавлен кэш, запросы в НСИ раз в 15 минут

This commit is contained in:
Дмитрий Соловьев
2026-05-26 12:41:08 +03:00
parent cbbafebf79
commit 4edf9d7aa8
8 changed files with 135 additions and 9 deletions
@@ -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>
)
@@ -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)
}
}
@@ -53,6 +53,7 @@ class ExternalPointsClientTest {
mock(PlatformsClassifierClient::class.java),
ClassifierProperties(
platformsUrl = "http://localhost",
platformsCacheTtl = java.time.Duration.ofMinutes(15),
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
)
) {
@@ -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")
)
)
@@ -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)
}
}
}
@@ -226,6 +226,7 @@ class SatelliteMissionProcessServiceTest {
mock(PlatformsClassifierClient::class.java),
ClassifierProperties(
platformsUrl = "http://localhost",
platformsCacheTtl = java.time.Duration.ofMinutes(15),
allowedPlatformStatuses = setOf("OPERATIONAL", "STANDBY")
)
) {