diff --git a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt index 91b4e2dc..73549c1b 100644 --- a/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt +++ b/service/src/main/kotlin/com/sangdol/roomescape/schedule/business/ScheduleService.kt @@ -71,10 +71,11 @@ class ScheduleService( id = id, currentStatus = ScheduleStatus.AVAILABLE, changeStatus = ScheduleStatus.HOLD - ) + ).also { + log.info { "[ScheduleService.holdSchedule] $it 개의 row 변경 완료" } + } if (result == 0) { - log.info { "[ScheduleService.holdSchedule] Holding된 일정 없음: id=${id}, count = " } throw ScheduleException(ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE) } diff --git a/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt new file mode 100644 index 00000000..c237d11e --- /dev/null +++ b/service/src/test/kotlin/com/sangdol/roomescape/schedule/ScheduleConcurrencyTest.kt @@ -0,0 +1,59 @@ +package com.sangdol.roomescape.schedule + +import com.sangdol.common.types.web.HttpStatus +import com.sangdol.roomescape.schedule.exception.ScheduleErrorCode +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository +import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus +import com.sangdol.roomescape.supports.FunSpecSpringbootTest +import com.sangdol.roomescape.supports.runTest +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import org.springframework.data.repository.findByIdOrNull +import java.util.concurrent.CountDownLatch + +class ScheduleConcurrencyTest( + private val scheduleRepository: ScheduleRepository +) : FunSpecSpringbootTest() { + init { + test("하나의 ${ScheduleStatus.AVAILABLE}인 일정에 대한 동시 HOLD 변경 요청이 들어오면, 가장 먼저 들어온 요청만 성공한다.") { + val user = testAuthUtil.defaultUserLogin() + val schedule = dummyInitializer.createSchedule() + + withContext(Dispatchers.IO) { + val jobSize = 64 + val latch = CountDownLatch(jobSize) + + (1..jobSize).map { + async { + latch.countDown() + latch.await() + + runTest( + token = user.second, + on = { + post("/schedules/${schedule.id}/hold") + }, + expect = { + } + ).extract().statusCode() + } + }.awaitAll().also { + it.count { statusCode -> statusCode == HttpStatus.OK.value() } shouldBe 1 + it.count { statusCode -> statusCode == ScheduleErrorCode.SCHEDULE_NOT_AVAILABLE.httpStatus.value() } shouldBe (jobSize - 1) + } + } + + assertSoftly(scheduleRepository.findByIdOrNull(schedule.id)!!) { + this.status shouldBe ScheduleStatus.HOLD + this.holdExpiredAt.shouldNotBeNull() + } + } + } +}