Skip to content

Commit

Permalink
feat/126 check wait queue (#134)
Browse files Browse the repository at this point in the history
* feat: apply waiting queue

* feat: change check logic

* test: add test

* chore: add env

* docs: update open-api.yaml

---------

Co-authored-by: Git Actions <no-reply@github.com>
  • Loading branch information
minjun3021 and Git Actions authored Nov 5, 2023
1 parent 5084bca commit 7c6574f
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 108 deletions.
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ services:
JWT_SECRET: d2VhcmVuYXZ5c3dkZXZlbG9wZXJzLmFuZGlhbW1pbmp1bjMwMjE=
JWT_EXPIRATION_HOURS: 24
JWT_ISSUER: minjun
QUEUE_SERVER_URL : http://localhost
SPRING_PROFILES_ACTIVE: local
depends_on:
db:
Expand Down
4 changes: 2 additions & 2 deletions docs/open-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -519,10 +519,10 @@ components:
pageSize:
type: integer
format: int32
paged:
type: boolean
unpaged:
type: boolean
paged:
type: boolean
Reservation:
required:
- address
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package com.group4.ticketingservice.Reservation

import com.group4.ticketingservice.AbstractIntegrationTest
import com.group4.ticketingservice.dto.QueueResponseDTO
import com.group4.ticketingservice.entity.Event
import com.group4.ticketingservice.entity.User
import com.group4.ticketingservice.repository.EventRepository
import com.group4.ticketingservice.repository.ReservationRepository
import com.group4.ticketingservice.repository.UserRepository
import com.group4.ticketingservice.service.ReservationService
import io.mockk.every
import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.RepeatedTest
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpMethod
import org.springframework.http.ResponseEntity
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.test.context.TestPropertySource
import org.springframework.web.client.RestTemplate
import java.time.Duration.ofHours
import java.time.OffsetDateTime
import java.util.concurrent.CountDownLatch
Expand Down Expand Up @@ -50,6 +56,12 @@ class ReservationTest @Autowired constructor(
maxAttendees = 100
)

private val queueSuccess: QueueResponseDTO = QueueResponseDTO(
status = true,
message = "",
data = null
)

@BeforeEach fun addUserAndEvent() {
userRepository.save(sampleUser)
eventRepository.save(sampleEvent)
Expand All @@ -67,6 +79,9 @@ class ReservationTest @Autowired constructor(
@RepeatedTest(3)
@Test
fun `ReservationService_createReservation should not exceed the limit in the concurrency test`() {
val restTemplate: RestTemplate = mockk()
every { restTemplate.exchange(any() as String, HttpMethod.DELETE, null, QueueResponseDTO::class.java) } returns ResponseEntity.ok(queueSuccess)
reservationService.restTemplate = restTemplate
val threadCount = 1000
val executorService = Executors.newFixedThreadPool(32)
val countDownLatch = CountDownLatch(threadCount)
Expand Down
4 changes: 4 additions & 0 deletions src/integrationTest/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ ticketing:
secret: d2VhcmVuYXZ5c3dkZXZlbG9wZXJzLmFuZGlhbW1pbmp1bjMwMjE=
expiration-hours: '24'
issuer: minjun

queue:
server:
url: http://localhost:8082/ticket
6 changes: 6 additions & 0 deletions src/main/kotlin/com/group4/ticketingservice/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.group4.ticketingservice.config
import org.modelmapper.ModelMapper
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestTemplate

@Configuration
class Config {
Expand All @@ -13,4 +14,9 @@ class Config {
modelMapper.configuration.isFieldMatchingEnabled = true
return modelMapper
}

@Bean
fun restTemplate(): RestTemplate {
return RestTemplate()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,107 +26,111 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/reservations")
class ReservationController(val reservationService: ReservationService) {
@PostMapping
fun createReservation(
@AuthenticationPrincipal userId: Int,
@RequestBody @Valid
request: ReservationCreateRequest
): ResponseEntity<ReservationResponse> {
val reservation: Reservation = reservationService.createReservation(
request.eventId!!,
userId,
request.name!!,
request.phoneNumber!!,
request.postCode!!,
request.address!!
)
val response = ReservationResponse(
id = reservation.id!!,
eventId = reservation.event.id!!,
userId = reservation.user.id!!,
createdAt = reservation.createdAt,
name = reservation.name,
phoneNumber = reservation.phoneNumber,
address = reservation.address,
postCode = reservation.postCode
)

val headers = HttpHeaders()
headers.set("Content-Location", "/reservations/%d".format(reservation.id!!))

return ResponseEntity(response, headers, HttpStatus.CREATED)
}

@GetMapping("/{id}")
fun getReservation(
request: HttpServletRequest,
@PathVariable id: Int
): ResponseEntity<ReservationResponse> {
val reservation = reservationService.getReservation(id)

val response = ReservationResponse(
id = reservation.id!!,
eventId = reservation.event.id!!,
userId = reservation.user.id!!,
createdAt = reservation.createdAt,
name = reservation.name,
phoneNumber = reservation.phoneNumber,
address = reservation.address,
postCode = reservation.postCode
)

val headers = HttpHeaders()
headers.set("Content-Location", request.requestURI)

return ResponseEntity(response, headers, HttpStatus.OK)
}

@PutMapping("/{id}")
fun updateReservation(
request: HttpServletRequest,
@PathVariable id: Int,
@RequestBody @Valid
reservationRequest: ReservationUpdateRequest
): ResponseEntity<ReservationResponse> {
val reservation = reservationService.updateReservation(id, reservationRequest.eventId!!)

val response = ReservationResponse(
id = reservation.id!!,
eventId = reservation.event.id!!,
userId = reservation.user.id!!,
createdAt = reservation.createdAt,
name = reservation.name,
phoneNumber = reservation.phoneNumber,
address = reservation.address,
postCode = reservation.postCode
)

val headers = HttpHeaders()
headers.set("Content-Location", request.requestURI)

return ResponseEntity(response, headers, HttpStatus.OK)
}

@DeleteMapping("/{id}")
fun deleteReservation(
@AuthenticationPrincipal userId: Int,
@PathVariable id: Int
): ResponseEntity<Any> {
reservationService.deleteReservation(userId, id)
return ResponseEntity(null, HttpStatus.OK)
}

@GetMapping
fun getReservations(
request: HttpServletRequest,
@AuthenticationPrincipal userId: Int,
@PageableDefault(size = 10) pageable: Pageable
): ResponseEntity<Page<Reservation>> {
val page = reservationService.getReservations(userId, pageable)

val headers = HttpHeaders()
headers.set("Content-Location", request.requestURI)

return ResponseEntity(page, headers, HttpStatus.OK)
@RestController
@RequestMapping("/reservations")
class ReservationController(val reservationService: ReservationService) {
@PostMapping
fun createReservation(
@AuthenticationPrincipal userId: Int,
@RequestBody @Valid
request: ReservationCreateRequest
): ResponseEntity<ReservationResponse> {
val reservation: Reservation = reservationService.createReservation(
request.eventId!!,
userId,
request.name!!,
request.phoneNumber!!,
request.postCode!!,
request.address!!
)
val response = ReservationResponse(
id = reservation.id!!,
eventId = reservation.event.id!!,
userId = reservation.user.id!!,
createdAt = reservation.createdAt,
name = reservation.name,
phoneNumber = reservation.phoneNumber,
address = reservation.address,
postCode = reservation.postCode
)

val headers = HttpHeaders()
headers.set("Content-Location", "/reservations/%d".format(reservation.id!!))

return ResponseEntity(response, headers, HttpStatus.CREATED)
}

@GetMapping("/{id}")
fun getReservation(
request: HttpServletRequest,
@PathVariable id: Int
): ResponseEntity<ReservationResponse> {
val reservation = reservationService.getReservation(id)

val response = ReservationResponse(
id = reservation.id!!,
eventId = reservation.event.id!!,
userId = reservation.user.id!!,
createdAt = reservation.createdAt,
name = reservation.name,
phoneNumber = reservation.phoneNumber,
address = reservation.address,
postCode = reservation.postCode
)

val headers = HttpHeaders()
headers.set("Content-Location", request.requestURI)

return ResponseEntity(response, headers, HttpStatus.OK)
}

@PutMapping("/{id}")
fun updateReservation(
request: HttpServletRequest,
@PathVariable id: Int,
@RequestBody @Valid
reservationRequest: ReservationUpdateRequest
): ResponseEntity<ReservationResponse> {
val reservation = reservationService.updateReservation(id, reservationRequest.eventId!!)

val response = ReservationResponse(
id = reservation.id!!,
eventId = reservation.event.id!!,
userId = reservation.user.id!!,
createdAt = reservation.createdAt,
name = reservation.name,
phoneNumber = reservation.phoneNumber,
address = reservation.address,
postCode = reservation.postCode
)

val headers = HttpHeaders()
headers.set("Content-Location", request.requestURI)

return ResponseEntity(response, headers, HttpStatus.OK)
}

@DeleteMapping("/{id}")
fun deleteReservation(
@AuthenticationPrincipal userId: Int,
@PathVariable id: Int
): ResponseEntity<Any> {
reservationService.deleteReservation(userId, id)
return ResponseEntity(null, HttpStatus.OK)
}

@GetMapping
fun getReservations(
request: HttpServletRequest,
@AuthenticationPrincipal userId: Int,
@PageableDefault(size = 10) pageable: Pageable
): ResponseEntity<Page<Reservation>> {
val page = reservationService.getReservations(userId, pageable)

val headers = HttpHeaders()
headers.set("Content-Location", request.requestURI)

return ResponseEntity(page, headers, HttpStatus.OK)
}
}
}
18 changes: 18 additions & 0 deletions src/main/kotlin/com/group4/ticketingservice/dto/QueueDTO.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.group4.ticketingservice.dto

class QueueResponseDTO(
val status: Boolean = false,
val message: String = "",
val data: TicketInfo? = null
)

class TicketInfo(
var eventId: String = "",
var userId: String = "",
var isWaiting: Boolean = true,
var offset: Int = 0
)
data class TicketRequest(
val eventId: Int,
val userId: Int
)
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
package com.group4.ticketingservice.service

import com.group4.ticketingservice.dto.QueueResponseDTO
import com.group4.ticketingservice.entity.Reservation
import com.group4.ticketingservice.repository.EventRepository
import com.group4.ticketingservice.repository.ReservationRepository
import com.group4.ticketingservice.repository.UserRepository
import com.group4.ticketingservice.utils.exception.CustomException
import com.group4.ticketingservice.utils.exception.ErrorCodes
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.client.HttpClientErrorException
import org.springframework.web.client.RestTemplate
import java.time.OffsetDateTime

@Service
class ReservationService @Autowired constructor(
private val userRepository: UserRepository,
private val eventRepository: EventRepository,
private val reservationRepository: ReservationRepository
private val reservationRepository: ReservationRepository,
var restTemplate: RestTemplate,
@Value("\${ticketing.queue.server.url}")
private val queueServerURL: String

) {
@Transactional
fun createReservation(eventId: Int, userId: Int, name: String, phoneNumber: String, postCode: Int, address: String): Reservation {
try {
restTemplate.exchange("$queueServerURL/running/$eventId/$userId", HttpMethod.DELETE, null, QueueResponseDTO::class.java).body
} catch (e: HttpClientErrorException) {
throw CustomException(ErrorCodes.WAITING_TICKET_NOT_FOUND)
}

val user = userRepository.getReferenceById(userId)
val event = eventRepository.findByIdWithPesimisticLock(eventId) ?: throw CustomException(ErrorCodes.ENTITY_NOT_FOUND)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum class ErrorCodes(val status: HttpStatus, val message: String, val errorCode
// 404 Not Found
ENTITY_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 레코드를 찾을수 없습니다.", 40000),
END_POINT_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 엔드포인트를 찾을수업습니다.", 40400),
WAITING_TICKET_NOT_FOUND(HttpStatus.NOT_FOUND, "대기열 티켓이 존재하지않아 예약 불가합니다.", 40402),
TEST_ERROR(HttpStatus.NOT_FOUND, "테스트 입니다.", 40401),

// 409 Conflict
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ management:
include: prometheus
prometheus:
enabled: 'true'

ticketing:
queue:
server:
url: ${QUEUE_SERVER_URL}
Loading

0 comments on commit 7c6574f

Please sign in to comment.