diff --git a/services-queue/build.gradle.kts b/services-queue/build.gradle.kts index c4cb0c7..e149428 100644 --- a/services-queue/build.gradle.kts +++ b/services-queue/build.gradle.kts @@ -10,4 +10,6 @@ dependencies { implementation("org.springframework.kafka:spring-kafka") implementation("org.springframework.boot:spring-boot-starter-webflux") testImplementation("io.projectreactor:reactor-test") + + testImplementation("io.mockk:mockk:1.13.8") } diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/PlatformInfo.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/PlatformInfo.kt new file mode 100644 index 0000000..1fa2de5 --- /dev/null +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/PlatformInfo.kt @@ -0,0 +1,7 @@ +package com.yourssu.morupark.queue.business + +data class PlatformInfo( + val platformId: Long, + val platformName: String, + val tps: Long, +) diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/QueueService.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/QueueService.kt index abe760c..1bf45c2 100644 --- a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/QueueService.kt +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/QueueService.kt @@ -15,18 +15,16 @@ class QueueService( ) { fun enqueue(accessToken: String) { - if (!authAdapter.isTokenValid(accessToken)) { - throw IllegalStateException("Access token is invalid") - } + // Validate token by getting user info + authAdapter.getUserInfo(accessToken) kafkaProducer.send(accessToken) } fun getWaitingToken(accessToken: String): String { - if (!authAdapter.isTokenValid(accessToken)) { - throw IllegalStateException("Access token is invalid") - } + val userInfo = authAdapter.getUserInfo(accessToken) + val platformId = userInfo.platform.platformId - if (!queueAdapter.isInQueue(accessToken)) { + if (!queueAdapter.isInQueue(accessToken, platformId)) { throw IllegalStateException("Access token is not in queue") } @@ -34,16 +32,19 @@ class QueueService( } fun getTicketStatusResult(accessToken: String, waitingToken: String): Any { - val ticketStatus = queueAdapter.getTicketStatus(accessToken) + val userInfo = authAdapter.getUserInfo(accessToken) + val platformId = userInfo.platform.platformId + + val ticketStatus = queueAdapter.getTicketStatus(accessToken, platformId) if (ticketStatus == TicketStatus.ALLOWED) { - queueAdapter.deleteFromAllowedQueue(accessToken) + queueAdapter.deleteFromAllowedQueue(accessToken, platformId) return getAllowedStatusResult(waitingToken) } - return getWaitingStatusResult(accessToken) + return getWaitingStatusResult(accessToken, platformId) } - private fun getWaitingStatusResult(accessToken: String): ReadWaitingStatusResult { - val rank = queueAdapter.getRank(accessToken)!! + private fun getWaitingStatusResult(accessToken: String, platformId: Long): ReadWaitingStatusResult { + val rank = queueAdapter.getRank(accessToken, platformId)!! val estimatedWaitingTime = waitingTimeEstimator.estimateWaitingTime(rank) return ReadWaitingStatusResult(TicketStatus.WAITING, rank, estimatedWaitingTime) } diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/UserInfo.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/UserInfo.kt new file mode 100644 index 0000000..73f8531 --- /dev/null +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/business/UserInfo.kt @@ -0,0 +1,6 @@ +package com.yourssu.morupark.queue.business + +data class UserInfo( + val userId: Long, + val platform: PlatformInfo +) diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/AuthAdapter.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/AuthAdapter.kt index cbe955d..d142a8b 100644 --- a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/AuthAdapter.kt +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/AuthAdapter.kt @@ -1,5 +1,6 @@ package com.yourssu.morupark.queue.implement +import com.yourssu.morupark.queue.business.UserInfo import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient @@ -10,22 +11,21 @@ class AuthAdapter( private val AUTH_SERVICE_URL = "http://localhost:8081" private val webClientAuth = webClient.baseUrl(AUTH_SERVICE_URL).build() - fun isTokenValid(accessToken: String): Boolean { + fun getUserInfo(accessToken: String): UserInfo { return webClientAuth.get() .uri { uriBuilder -> - uriBuilder.path("/auth/token") + uriBuilder.path("/auth/me") .queryParam("accessToken", accessToken) .build() } .retrieve() - .bodyToMono(Boolean::class.java) - .block() == true + .bodyToMono(UserInfo::class.java) + .block() ?: throw IllegalStateException("Failed to get user info") } fun getWaitingToken(accessToken: String): String { return webClientAuth.get() .uri { uriBuilder -> - // TODO: 레오에게 이거 엔드포인트 뭔지 물어보기 uriBuilder.path("/auth/waiting-token") .queryParam("accessToken", accessToken) .build() @@ -38,8 +38,7 @@ class AuthAdapter( fun getExternalServerToken(waitingToken: String): String { return webClientAuth.get() .uri { uriBuilder -> - // TODO: 레오에게 이거 엔드포인트 뭔지 물어보기 - uriBuilder.path("/auth/external-sever-token") + uriBuilder.path("/platforms/url") .queryParam("waitingToken", waitingToken) .build() } diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/KafkaConsumer.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/KafkaConsumer.kt index 5f1668e..55d9a25 100644 --- a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/KafkaConsumer.kt +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/KafkaConsumer.kt @@ -8,8 +8,8 @@ import org.springframework.stereotype.Component @Component class KafkaConsumer( private val queueAdapter: QueueAdapter, - - ) { + private val authAdapter: AuthAdapter, +) { companion object { private const val TAG = "WAITING" @@ -21,8 +21,11 @@ class KafkaConsumer( accessToken: String, @Header(KafkaHeaders.TIMESTAMP) timestamp: Long, ) { - - queueAdapter.addToWaitingQueue(accessToken, timestamp) + val userInfo = authAdapter.getUserInfo(accessToken) + val platformId = userInfo.platform.platformId + val tps = userInfo.platform.tps + ServerTPSMap.put(platformId, tps) + queueAdapter.addToWaitingQueue(accessToken, timestamp, platformId) } } diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/QueueAdapter.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/QueueAdapter.kt index c1a688a..049a19f 100644 --- a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/QueueAdapter.kt +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/QueueAdapter.kt @@ -2,6 +2,7 @@ package com.yourssu.morupark.queue.implement import com.yourssu.morupark.queue.business.TicketStatus import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.core.ScanOptions import org.springframework.stereotype.Component @Component @@ -9,44 +10,61 @@ class QueueAdapter( private val redisTemplate: RedisTemplate ) { - private val QUEUE_WAITING_KEY = "queue:waiting" - private val QUEUE_ALLOWED_KEY = "queue:allowed" + private fun getWaitingKey(platformId: Long) = "queue:$platformId:waiting" + private fun getAllowedKey(platformId: Long) = "queue:$platformId:allowed" - fun addToWaitingQueue(accessToken: String, timestamp: Long) { - redisTemplate.opsForZSet().add(QUEUE_WAITING_KEY, accessToken, timestamp.toDouble()) + fun addToWaitingQueue(accessToken: String, timestamp: Long, platformId: Long) { + redisTemplate.opsForZSet().add(getWaitingKey(platformId), accessToken, timestamp.toDouble()) } - fun addToAllowedQueue(accessTokens: Set) { - redisTemplate.opsForSet().add(QUEUE_ALLOWED_KEY, *accessTokens.toTypedArray()) + fun addToAllowedQueue(accessTokens: Set, platformId: Long) { + redisTemplate.opsForSet().add(getAllowedKey(platformId), *accessTokens.toTypedArray()) } - fun popFromWaitingQueue(count: Long): Set? { - val items = redisTemplate.opsForZSet().range(QUEUE_WAITING_KEY, 0, count - 1) - redisTemplate.opsForZSet().remove(QUEUE_WAITING_KEY, items) + fun popFromWaitingQueue(count: Long, platformId: Long): Set? { + val waitingKey = getWaitingKey(platformId) + val items = redisTemplate.opsForZSet().range(waitingKey, 0, count - 1) + if (!items.isNullOrEmpty()) { + redisTemplate.opsForZSet().remove(waitingKey, *items.toTypedArray()) + } return items } - fun deleteFromAllowedQueue(accessToken: String) { - redisTemplate.opsForSet().remove(QUEUE_ALLOWED_KEY, accessToken) + fun deleteFromAllowedQueue(accessToken: String, platformId: Long) { + redisTemplate.opsForSet().remove(getAllowedKey(platformId), accessToken) } - fun isInQueue(accessToken: String): Boolean { - val score = redisTemplate.opsForZSet().score(QUEUE_WAITING_KEY, accessToken) + fun isInQueue(accessToken: String, platformId: Long): Boolean { + val score = redisTemplate.opsForZSet().score(getWaitingKey(platformId), accessToken) if (score != null) { return true } - return redisTemplate.opsForSet().isMember(QUEUE_ALLOWED_KEY, accessToken)!! + return redisTemplate.opsForSet().isMember(getAllowedKey(platformId), accessToken)!! } - fun getTicketStatus(accessToken: String): TicketStatus { - val rank = redisTemplate.opsForZSet().rank(QUEUE_WAITING_KEY, accessToken) + fun getTicketStatus(accessToken: String, platformId: Long): TicketStatus { + val rank = redisTemplate.opsForZSet().rank(getWaitingKey(platformId), accessToken) if (rank != null) return TicketStatus.WAITING - val allowed = redisTemplate.opsForSet().isMember(QUEUE_ALLOWED_KEY, accessToken) + val allowed = redisTemplate.opsForSet().isMember(getAllowedKey(platformId), accessToken) if (allowed != null && allowed) return TicketStatus.ALLOWED throw IllegalStateException("현재 대기열에 존재하지 않습니다.") } - fun getRank(accessToken: String): Long? { - return redisTemplate.opsForZSet().rank(QUEUE_WAITING_KEY, accessToken) + fun getRank(accessToken: String, platformId: Long): Long? { + return redisTemplate.opsForZSet().rank(getWaitingKey(platformId), accessToken) + } + + fun getAllPlatformWaitingKeys(): Set { + val keys = mutableSetOf() + val scanOptions = ScanOptions.scanOptions().match("queue:*:waiting").count(100).build() + redisTemplate.scan(scanOptions).use { cursor -> + cursor.forEach { key -> keys.add(key) } + } + return keys + } + + fun extractPlatformIdFromKey(key: String): Long { + val parts = key.split(":") + return parts[1].toLong() } } diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/ServerTPSMap.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/ServerTPSMap.kt new file mode 100644 index 0000000..0afd03a --- /dev/null +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/ServerTPSMap.kt @@ -0,0 +1,13 @@ +package com.yourssu.morupark.queue.implement + +object ServerTPSMap { + private val tpsMap = mutableMapOf() + + fun put(platformId: Long, tps: Long) { + tpsMap[platformId] = tps + } + + fun get(platformId: Long): Long? { + return tpsMap[platformId] + } +} \ No newline at end of file diff --git a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/StatusOperator.kt b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/StatusOperator.kt index b8a175f..6fc334e 100644 --- a/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/StatusOperator.kt +++ b/services-queue/src/main/kotlin/com/yourssu/morupark/queue/implement/StatusOperator.kt @@ -15,11 +15,20 @@ class StatusOperator( /** * 시간당 상태를 Waiting -> Allowed로 바꿔준다. + * 모든 플랫폼의 대기열을 Redis 키 패턴 스캔으로 찾아서 처리한다. + * 각 플랫폼의 TPS 설정에 따라 처리량을 조절한다. */ @Scheduled(fixedDelayString = "\${queue.processing-interval}") fun changeStatus() { - val accessTokens = queueAdapter.popFromWaitingQueue(maxSize) - if (!accessTokens.isNullOrEmpty()) - queueAdapter.addToAllowedQueue(accessTokens) + val waitingKeys = queueAdapter.getAllPlatformWaitingKeys() + + for (key in waitingKeys) { + val platformId = queueAdapter.extractPlatformIdFromKey(key) + val tps = ServerTPSMap.get(platformId) ?: maxSize + val accessTokens = queueAdapter.popFromWaitingQueue(tps, platformId) + if (!accessTokens.isNullOrEmpty()) { + queueAdapter.addToAllowedQueue(accessTokens, platformId) + } + } } } diff --git a/services-queue/src/test/kotlin/com/yourssu/morupark/queue/business/QueueServiceTest.kt b/services-queue/src/test/kotlin/com/yourssu/morupark/queue/business/QueueServiceTest.kt index 80f2ba7..f1ea1b5 100644 --- a/services-queue/src/test/kotlin/com/yourssu/morupark/queue/business/QueueServiceTest.kt +++ b/services-queue/src/test/kotlin/com/yourssu/morupark/queue/business/QueueServiceTest.kt @@ -35,40 +35,50 @@ class QueueServiceTest { private val accessToken = "valid-access-token" private val waitingToken = "waiting-token" private val externalServerToken = "external-server-token" + private val platformId = 1L + private val platformName = "test-platform" + private val tps = 100L + + private fun createUserInfo(): UserInfo { + return UserInfo( + userId = 1L, + platform = PlatformInfo(platformId, platformName, tps) + ) + } @Test fun `유효한 토큰으로 큐에 등록한다`() { // given - every { authAdapter.isTokenValid(accessToken) } returns true + every { authAdapter.getUserInfo(accessToken) } returns createUserInfo() every { kafkaProducer.send(accessToken) } just runs // when queueService.enqueue(accessToken) // then - verify { authAdapter.isTokenValid(accessToken) } + verify { authAdapter.getUserInfo(accessToken) } verify { kafkaProducer.send(accessToken) } } @Test fun `유효하지 않은 토큰으로 큐 등록 시 예외가 발생한다`() { // given - every { authAdapter.isTokenValid(accessToken) } returns false + every { authAdapter.getUserInfo(accessToken) } throws IllegalStateException("Failed to get user info") // when & then val exception = assertFailsWith { queueService.enqueue(accessToken) } - assertEquals("Access token is invalid", exception.message) - verify { authAdapter.isTokenValid(accessToken) } + assertEquals("Failed to get user info", exception.message) + verify { authAdapter.getUserInfo(accessToken) } verify(exactly = 0) { kafkaProducer.send(any()) } } @Test fun `유효한 토큰과 큐에 있는 사용자의 대기 토큰을 반환한다`() { // given - every { authAdapter.isTokenValid(accessToken) } returns true - every { queueAdapter.isInQueue(accessToken) } returns true + every { authAdapter.getUserInfo(accessToken) } returns createUserInfo() + every { queueAdapter.isInQueue(accessToken, platformId) } returns true every { authAdapter.getWaitingToken(accessToken) } returns waitingToken // when @@ -76,47 +86,48 @@ class QueueServiceTest { // then assertEquals(waitingToken, result) - verify { authAdapter.isTokenValid(accessToken) } - verify { queueAdapter.isInQueue(accessToken) } + verify { authAdapter.getUserInfo(accessToken) } + verify { queueAdapter.isInQueue(accessToken, platformId) } verify { authAdapter.getWaitingToken(accessToken) } } @Test fun `유효하지 않은 토큰으로 대기 토큰 요청 시 예외가 발생한다`() { // given - every { authAdapter.isTokenValid(accessToken) } returns false + every { authAdapter.getUserInfo(accessToken) } throws IllegalStateException("Failed to get user info") // when & then val exception = assertFailsWith { queueService.getWaitingToken(accessToken) } - assertEquals("Access token is invalid", exception.message) - verify { authAdapter.isTokenValid(accessToken) } - verify(exactly = 0) { queueAdapter.isInQueue(any()) } + assertEquals("Failed to get user info", exception.message) + verify { authAdapter.getUserInfo(accessToken) } + verify(exactly = 0) { queueAdapter.isInQueue(any(), any()) } verify(exactly = 0) { authAdapter.getWaitingToken(any()) } } @Test fun `큐에 없는 사용자의 대기 토큰 요청 시 예외가 발생한다`() { // given - every { authAdapter.isTokenValid(accessToken) } returns true - every { queueAdapter.isInQueue(accessToken) } returns false + every { authAdapter.getUserInfo(accessToken) } returns createUserInfo() + every { queueAdapter.isInQueue(accessToken, platformId) } returns false // when & then val exception = assertFailsWith { queueService.getWaitingToken(accessToken) } assertEquals("Access token is not in queue", exception.message) - verify { authAdapter.isTokenValid(accessToken) } - verify { queueAdapter.isInQueue(accessToken) } + verify { authAdapter.getUserInfo(accessToken) } + verify { queueAdapter.isInQueue(accessToken, platformId) } verify(exactly = 0) { authAdapter.getWaitingToken(any()) } } @Test fun `ALLOWED 상태인 사용자는 허용된 상태 결과를 반환한다`() { // given - every { queueAdapter.getTicketStatus(accessToken) } returns TicketStatus.ALLOWED - every { queueAdapter.deleteFromAllowedQueue(accessToken) } just runs + every { authAdapter.getUserInfo(accessToken) } returns createUserInfo() + every { queueAdapter.getTicketStatus(accessToken, platformId) } returns TicketStatus.ALLOWED + every { queueAdapter.deleteFromAllowedQueue(accessToken, platformId) } just runs every { authAdapter.getExternalServerToken(waitingToken) } returns externalServerToken // when @@ -127,8 +138,9 @@ class QueueServiceTest { assertEquals(TicketStatus.ALLOWED, result.status) assertEquals(externalServerToken, result.externalServerToken) - verify { queueAdapter.getTicketStatus(accessToken) } - verify { queueAdapter.deleteFromAllowedQueue(accessToken) } + verify { authAdapter.getUserInfo(accessToken) } + verify { queueAdapter.getTicketStatus(accessToken, platformId) } + verify { queueAdapter.deleteFromAllowedQueue(accessToken, platformId) } verify { authAdapter.getExternalServerToken(waitingToken) } } @@ -137,8 +149,9 @@ class QueueServiceTest { // given val rank = 10L val estimatedWaitingTime = 300L - every { queueAdapter.getTicketStatus(accessToken) } returns TicketStatus.WAITING - every { queueAdapter.getRank(accessToken) } returns rank + every { authAdapter.getUserInfo(accessToken) } returns createUserInfo() + every { queueAdapter.getTicketStatus(accessToken, platformId) } returns TicketStatus.WAITING + every { queueAdapter.getRank(accessToken, platformId) } returns rank every { waitingTimeEstimator.estimateWaitingTime(rank) } returns estimatedWaitingTime // when @@ -150,9 +163,10 @@ class QueueServiceTest { assertEquals(rank, result.rank) assertEquals(estimatedWaitingTime, result.estimatedWaitingTime) - verify { queueAdapter.getTicketStatus(accessToken) } - verify { queueAdapter.getRank(accessToken) } + verify { authAdapter.getUserInfo(accessToken) } + verify { queueAdapter.getTicketStatus(accessToken, platformId) } + verify { queueAdapter.getRank(accessToken, platformId) } verify { waitingTimeEstimator.estimateWaitingTime(rank) } - verify(exactly = 0) { queueAdapter.deleteFromAllowedQueue(any()) } + verify(exactly = 0) { queueAdapter.deleteFromAllowedQueue(any(), any()) } } -} \ No newline at end of file +} diff --git a/services-queue/src/test/kotlin/com/yourssu/morupark/queue/implement/QueueAdapterTest.kt b/services-queue/src/test/kotlin/com/yourssu/morupark/queue/implement/QueueAdapterTest.kt index 2a77aa5..a9b6833 100644 --- a/services-queue/src/test/kotlin/com/yourssu/morupark/queue/implement/QueueAdapterTest.kt +++ b/services-queue/src/test/kotlin/com/yourssu/morupark/queue/implement/QueueAdapterTest.kt @@ -5,10 +5,15 @@ import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK import io.mockk.junit5.MockKExtension +import io.mockk.just +import io.mockk.mockk +import io.mockk.Runs import io.mockk.verify import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.redis.core.Cursor import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.core.ScanOptions import org.springframework.data.redis.core.SetOperations import org.springframework.data.redis.core.ZSetOperations import kotlin.test.assertEquals @@ -33,8 +38,9 @@ class QueueAdapterTest { private lateinit var queueAdapter: QueueAdapter private val token = "token123" - private val QUEUE_WAITING_KEY = "queue:waiting" - private val QUEUE_ALLOWED_KEY = "queue:allowed" + private val platformId = 1L + private val QUEUE_WAITING_KEY = "queue:$platformId:waiting" + private val QUEUE_ALLOWED_KEY = "queue:$platformId:allowed" @Test fun `사용자가 대기열에 추가되면, waiting queue에 저장된다`() { @@ -44,7 +50,7 @@ class QueueAdapterTest { val timestamp = System.nanoTime() // when - queueAdapter.addToWaitingQueue(token, timestamp) + queueAdapter.addToWaitingQueue(token, timestamp, platformId) // then verify { @@ -60,7 +66,7 @@ class QueueAdapterTest { every { setOps.add(QUEUE_ALLOWED_KEY, *accessTokens.toTypedArray()) } returns 3L // when - queueAdapter.addToAllowedQueue(accessTokens) + queueAdapter.addToAllowedQueue(accessTokens, platformId) // then verify { setOps.add(QUEUE_ALLOWED_KEY, *accessTokens.toTypedArray()) } @@ -73,15 +79,31 @@ class QueueAdapterTest { val count = 5L val expectedItems = setOf("token1", "token2", "token3") every { zSetOps.range(QUEUE_WAITING_KEY, 0, count - 1) } returns expectedItems - every { zSetOps.remove(QUEUE_WAITING_KEY, expectedItems) } returns 3L + every { zSetOps.remove(QUEUE_WAITING_KEY, *expectedItems.toTypedArray()) } returns 3L // when - val result = queueAdapter.popFromWaitingQueue(count) + val result = queueAdapter.popFromWaitingQueue(count, platformId) // then assertEquals(expectedItems, result) verify { zSetOps.range(QUEUE_WAITING_KEY, 0, count - 1) } - verify { zSetOps.remove(QUEUE_WAITING_KEY, expectedItems) } + verify { zSetOps.remove(QUEUE_WAITING_KEY, *expectedItems.toTypedArray()) } + } + + @Test + fun `대기열이 비어있으면 빈 결과를 반환한다`() { + // given + every { redisTemplate.opsForZSet() } returns zSetOps + val count = 5L + every { zSetOps.range(QUEUE_WAITING_KEY, 0, count - 1) } returns emptySet() + + // when + val result = queueAdapter.popFromWaitingQueue(count, platformId) + + // then + assertTrue(result?.isEmpty() ?: true) + verify { zSetOps.range(QUEUE_WAITING_KEY, 0, count - 1) } + verify(exactly = 0) { zSetOps.remove(any(), *anyVararg()) } } @Test @@ -91,7 +113,7 @@ class QueueAdapterTest { every { setOps.remove(QUEUE_ALLOWED_KEY, token) } returns 1L // when - queueAdapter.deleteFromAllowedQueue(token) + queueAdapter.deleteFromAllowedQueue(token, platformId) // then verify { setOps.remove(QUEUE_ALLOWED_KEY, token) } @@ -105,7 +127,7 @@ class QueueAdapterTest { every { zSetOps.score(QUEUE_WAITING_KEY, token) } returns 1000.0 // when - val result = queueAdapter.isInQueue(token) + val result = queueAdapter.isInQueue(token, platformId) // then assertTrue(result) @@ -121,7 +143,7 @@ class QueueAdapterTest { every { setOps.isMember(QUEUE_ALLOWED_KEY, token) } returns true // when - val result = queueAdapter.isInQueue(token) + val result = queueAdapter.isInQueue(token, platformId) // then assertTrue(result) @@ -138,7 +160,7 @@ class QueueAdapterTest { every { setOps.isMember(QUEUE_ALLOWED_KEY, token) } returns false // when - val result = queueAdapter.isInQueue(token) + val result = queueAdapter.isInQueue(token, platformId) // then assertFalse(result) @@ -151,7 +173,7 @@ class QueueAdapterTest { every { zSetOps.rank(QUEUE_WAITING_KEY, token) } returns 5L // when - val result = queueAdapter.getTicketStatus(token) + val result = queueAdapter.getTicketStatus(token, platformId) // then assertEquals(TicketStatus.WAITING, result) @@ -166,7 +188,7 @@ class QueueAdapterTest { every { setOps.isMember(QUEUE_ALLOWED_KEY, token) } returns true // when - val result = queueAdapter.getTicketStatus(token) + val result = queueAdapter.getTicketStatus(token, platformId) // then assertEquals(TicketStatus.ALLOWED, result) @@ -181,7 +203,7 @@ class QueueAdapterTest { every { setOps.isMember(QUEUE_ALLOWED_KEY, token) } returns false // when - val exception = assertFailsWith { queueAdapter.getTicketStatus(token) } + val exception = assertFailsWith { queueAdapter.getTicketStatus(token, platformId) } // then assertEquals("현재 대기열에 존재하지 않습니다.", exception.message) @@ -195,7 +217,7 @@ class QueueAdapterTest { every { zSetOps.rank(QUEUE_WAITING_KEY, token) } returns expectedRank // when - val result = queueAdapter.getRank(token) + val result = queueAdapter.getRank(token, platformId) // then assertEquals(expectedRank, result) @@ -208,9 +230,41 @@ class QueueAdapterTest { every { zSetOps.rank(QUEUE_WAITING_KEY, token) } returns null // when - val result = queueAdapter.getRank(token) + val result = queueAdapter.getRank(token, platformId) // then assertNull(result) } + + @Test + fun `getAllPlatformWaitingKeys로 모든 플랫폼 대기열 키를 조회한다`() { + // given + val cursor = mockk>(relaxed = true) + val expectedKeys = listOf("queue:1:waiting", "queue:2:waiting", "queue:3:waiting") + val iterator = expectedKeys.iterator() + + every { redisTemplate.scan(any()) } returns cursor + every { cursor.hasNext() } answers { iterator.hasNext() } + every { cursor.next() } answers { iterator.next() } + every { cursor.close() } just Runs + + // when + val result = queueAdapter.getAllPlatformWaitingKeys() + + // then + assertEquals(expectedKeys.toSet(), result) + verify { cursor.close() } + } + + @Test + fun `extractPlatformIdFromKey로 키에서 플랫폼 ID를 추출한다`() { + // given + val key = "queue:123:waiting" + + // when + val result = queueAdapter.extractPlatformIdFromKey(key) + + // then + assertEquals(123L, result) + } }