Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 게임 엔진 서비스의 Busy Waiting 문제 개선 #83

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class QueueRepositoryAdapter(

override fun poll(roomId: Long, quizId: Long): AnswerResult? {
val key = "roomId : " + roomId.toString() + ", " + "quizId : " + quizId.toString()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 key가 여러 곳에서 쓰인다면 분리해보는 것도 좋을 것 같습니다.😀😀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요 레디스에서 다양한 키를 사용할 수 있고, 키 명명 규칙도 존재하기 때문에 별도의 파일에서 관리해주면 좋을 것 같습니다!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다! 이렇게 이슈가 늘어나는 거죠! ㅎㅎ 여기저기 쓰이면서 명명 규칙이 혼재해서 사용될 수 있기 때문에 분리해서 관리하는 게 좋을 것 같습니다.

return redisQueueRepository.poll(key)?.let {
return redisQueueRepository.poll(key, 1)?.let {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분이 1초를 대기하고 poll한다는 의미일까요?🤗🤗

Copy link
Contributor Author

@Jeongjjuna Jeongjjuna Sep 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1초동안 블럭킹으로 대기를하고, 없으면 null을 반환하도록 했습니다!
무한으로 blocking 해버리면 대기시간 10초를 카운트해서 다음 문제로 넘어가야하는데 그부분에서 문제가 생길 거같아서 일단 1초로 지정했습니다
이부분 한번 같이 이야기해보면 좋을거같아요!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 이해했습니다. 그리고 직접 게임 해보니까 10초가 참 짧게 느껴졌습니다. 대기시간 10초를 없애는 것도 같이 고려해봐도 좋을 것 같아요😀😀

objectMapper.readValue(it, AnswerResultEntity::class.java)?.toModel()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package yjh.cstar.game.infrastructure.redis

import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Repository
import java.util.concurrent.TimeUnit

@Repository
class RedisQueueRepository(
Expand All @@ -12,8 +13,8 @@ class RedisQueueRepository(
return redisTemplate.opsForList().rightPush(key, value)
}

fun poll(key: String): String? {
return redisTemplate.opsForList().leftPop(key)
fun poll(key: String, timeout: Long = 60, timeUnit: TimeUnit = TimeUnit.SECONDS): String? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout, timeunit이 어떤 차이가 있을까요? 🤗🤗

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout은 몇초동안 blocking을 할지 지정하는 파라미터이고, TimeUnit은 timout에 적어준 값의 기준값을 의미합니다.
예를들어 위의 코드에서 SECONDS로 설정되어있기 때문에, timout값이 10이라면 10초로 설정되는 것입니다!! 😀😀

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

친절하게 설명해주셔서 감사해요! 이해가 쏙쏙!! 되었습니다!

return redisTemplate.opsForList().leftPop(key, timeout, timeUnit)
}

fun getSize(key: String): Long? {
Expand Down
171 changes: 171 additions & 0 deletions src/test/kotlin/yjh/cstar/redis/RedisConcurrencyTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package yjh.cstar.redis

import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.GenericContainer
import org.testcontainers.utility.DockerImageName
import yjh.cstar.game.application.GameAnswerQueueService
import yjh.cstar.game.domain.AnswerResult
import yjh.cstar.game.infrastructure.redis.RedisQueueRepository
import java.util.Collections
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.assertEquals

@ActiveProfiles("local-test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DisplayName("[Redis 동시성 테스트] GameAnswerQueueService")
class RedisConcurrencyTest {

@Autowired
private lateinit var gameAnswerQueueService: GameAnswerQueueService

@Autowired
private lateinit var redisQueueRepository: RedisQueueRepository

companion object {
private const val ROOM_ID = 1L
private const val QUIZ_ID = 1L
private const val KEY = "roomId : " + ROOM_ID + ", " + "quizId : " + QUIZ_ID

private val redis: GenericContainer<*> = GenericContainer(DockerImageName.parse("redis:latest"))
.withExposedPorts(6379)
.withReuse(true)
Comment on lines +40 to +42
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 GenericContainer는 어떤 역할을 하는지 여쭤봐도 될까요?😆😆

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 구문은 컨테이너를 활용하기 위해 사용하였는데요, 특정 버전의 redis 이미지를 가져오고 컨테이너화 시킬 때 사용되는 클래스입니다! 😀😀

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컨테이너화... 오 좋은 구문이 많네요! 이렇게 또 배워갑니다!😀😀


@JvmStatic
@DynamicPropertySource
fun properties(registry: DynamicPropertyRegistry) {
registry.add("spring.data.redis.host", redis::getHost)
registry.add("spring.data.redis.port", redis::getFirstMappedPort)
}

@BeforeAll
@JvmStatic
fun beforeAll() {
redis.start()
}

@AfterAll
@JvmStatic
fun afterAll() {
redis.stop()
}
}

@BeforeTest
fun beforeEach() {
redisQueueRepository.deleteAll(KEY)
}

@AfterTest
fun afterEach() {
redisQueueRepository.deleteAll(KEY)
}

@Test
fun `레디스 사용자 퀴즈 정답 큐 동시성 테스트`() {
// given
val numberOfThreads = 2
val startLatch = CountDownLatch(1)
val doneLatch = CountDownLatch(numberOfThreads)
val executor = Executors.newFixedThreadPool(numberOfThreads)
val receive = Collections.synchronizedList(mutableListOf<AnswerResult>())

val answerResult = AnswerResult(
answer = "정답",
quizId = QUIZ_ID,
roomId = ROOM_ID,
playerId = 1,
nickname = "nickname"
)

// when
executor.submit {
try {
for (idx in 1..100) {
gameAnswerQueueService.add(answerResult)
}
} catch (e: Exception) {
println("Error: ${e.message}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

편의를 위해서 print문을 쓰신 걸까요? 그렇다면 남겨두는 것도 좋을 것 같습니다. 😁😁

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 편의를 위해서 사용하였는데, 혹시 나중에 성능적으로 문제가 될 것 같으면 로그를 찍는 방식으로 수정해봐도 좋을 것 같아요!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네네 알겠습니다. 나중에 print문이 있는 파트를 다 찾아서 로그로 바꿔봐도 좋을 것 같아요!

} finally {
doneLatch.countDown()
}
}

executor.submit {
try {
for (idx in 1..100) {
receive.add(gameAnswerQueueService.poll(ROOM_ID, QUIZ_ID))
}
} catch (e: Exception) {
println("Error: ${e.message}")
} finally {
doneLatch.countDown()
}
}

startLatch.countDown() // 모든 스레드 동시에 시작

doneLatch.await() // 모든 스레드 종료 대기

executor.shutdown()

// then
val pollSize = receive.filter { it != null }
.size
val remainPushSize = redisQueueRepository.getSize(KEY) ?: 0

assertEquals(100, pollSize + remainPushSize)
}

@Test
fun `레디스 List 삽입 동시성 테스트`() {
// given
val numberOfThreads = 100
val startLatch = CountDownLatch(1)
val doneLatch = CountDownLatch(numberOfThreads)
val executor = Executors.newFixedThreadPool(numberOfThreads)

val answerResult = AnswerResult(
answer = "정답",
quizId = QUIZ_ID,
roomId = ROOM_ID,
playerId = 1,
nickname = "nickname"
)

// when
// 데이터 추가
for (idx in 1..numberOfThreads) {
executor.submit {
try {
gameAnswerQueueService.add(answerResult)
} catch (e: Exception) {
println("Error: ${e.message}")
} finally {
doneLatch.countDown()
}
}
}

startLatch.countDown() // 모든 스레드 동시에 시작

doneLatch.await() // 모든 스레드 종료 대기

executor.shutdown()

// then
val remainPushSize = redisQueueRepository.getSize(KEY) ?: 0

assertEquals(100, remainPushSize)
}
}
17 changes: 17 additions & 0 deletions src/test/kotlin/yjh/cstar/redis/RedisTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,21 @@ class RedisTest {
val hasKey = redisTemplate.hasKey(KEY)
assertFalse(hasKey)
}

@Test
fun `레디스 Blocking Queue 동작 테스트`() {
// given
val value = objectMapper.writeValueAsString(AnswerResultEntity("ans_1", QUIZ_ID, ROOM_ID, 1, "nickname"))
repeat(5) {
redisQueueRepository.add(KEY, value)
}
val result = mutableListOf<String>()

// when
generateSequence { redisQueueRepository.poll(KEY, 5) }
.forEach { result.add(it) }

// then
assertEquals(5, result.size)
}
}