128 lines
5.1 KiB
Kotlin
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
|
|
)
|
|
}
|