Files
sodalive-backend-spring-boot/src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentPinConcurrencyTest.kt

128 lines
5.1 KiB
Kotlin

package kr.co.vividnext.sodalive.content
import kr.co.vividnext.sodalive.content.pin.PinContent
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.Assertions.assertEquals
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.ContextConfiguration
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import javax.persistence.EntityManager
@SpringBootTest(properties = ["cloud.aws.cloud-front.host=https://test.cloudfront.net"])
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@Transactional
class AudioContentPinConcurrencyTest @Autowired constructor(
private val service: AudioContentService,
private val entityManager: EntityManager,
private val transactionManager: PlatformTransactionManager
) {
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@DisplayName("동시 상단 고정 요청에서도 활성 고정은 9개를 넘지 않는다")
fun shouldKeepNineActivePinsWhenPinRequestsRace() {
val transactionTemplate = TransactionTemplate(transactionManager)
val fixture = transactionTemplate.execute {
val creator = saveMember("pin-race-creator", MemberRole.CREATOR)
val existingContents = (1..8).map { saveAudioContent(creator, "existing-$it") }
existingContents.forEach { savePinContent(creator, it) }
val first = saveAudioContent(creator, "first-race")
val second = saveAudioContent(creator, "second-race")
entityManager.flush()
PinRaceFixture(creator.id!!, first.id!!, second.id!!)
} ?: throw IllegalStateException("fixture creation failed")
val start = CountDownLatch(1)
val executor = Executors.newFixedThreadPool(2)
try {
val first = executor.submit {
start.await()
service.pinToTheTop(fixture.firstContentId, memberRef(fixture.creatorId))
}
val second = executor.submit {
start.await()
service.pinToTheTop(fixture.secondContentId, memberRef(fixture.creatorId))
}
start.countDown()
first.get(5, TimeUnit.SECONDS)
second.get(5, TimeUnit.SECONDS)
} finally {
executor.shutdownNow()
}
val activeContentIds = transactionTemplate.execute { activePinnedContentIds(fixture.creatorId) }.orEmpty()
assertEquals(9, activeContentIds.size)
assertEquals(activeContentIds.size, activeContentIds.distinct().size)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
role = role
)
entityManager.persist(member)
return member
}
private fun saveAudioContent(creator: Member, title: String): AudioContent {
val theme = AudioContentTheme(theme = "theme-$title", image = "$title.png", isActive = true)
entityManager.persist(theme)
val content = AudioContent(
title = title,
detail = "detail",
languageCode = "ko",
releaseDate = LocalDateTime.now().minusDays(1),
isAdult = false
)
content.member = creator
content.theme = theme
content.isActive = true
content.coverImage = "$title.png"
content.duration = "00:10:00"
entityManager.persist(content)
return content
}
private fun savePinContent(creator: Member, content: AudioContent): PinContent {
val pinContent = PinContent(isActive = true)
pinContent.member = creator
pinContent.content = content
entityManager.persist(pinContent)
return pinContent
}
private fun memberRef(memberId: Long): Member {
val member = Member(email = "creator-$memberId@test.com", password = "password", nickname = "creator-$memberId")
member.id = memberId
return member
}
private fun activePinnedContentIds(creatorId: Long): List<Long> {
return entityManager.createQuery(
"select p.content.id from PinContent p where p.member.id = :creatorId and p.isActive = true",
java.lang.Long::class.java
).setParameter("creatorId", creatorId).resultList.map { it.toLong() }
}
private data class PinRaceFixture(
val creatorId: Long,
val firstContentId: Long,
val secondContentId: Long
)
}