Skip to content

Commit

Permalink
Merge pull request #4 from Tiketeer/feat/DEV-243
Browse files Browse the repository at this point in the history
[DEV-243] Waiting 서버 유즈케이스 작성
  • Loading branch information
punkryn authored May 4, 2024
2 parents 7fe7b47 + 946a084 commit 70dee31
Show file tree
Hide file tree
Showing 16 changed files with 309 additions and 12 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Check PR

on:
pull_request

permissions:
checks: write
pull-requests: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 21
distribution: 'temurin'
cache: 'gradle'

- name: Check Build
run: ./gradlew clean build -x test

- name: Run test
run: ./gradlew test

- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
junit_files: '**/build/test-results/test/TEST-*.xml'
comment_mode: off

- name: Add coverage to PR
id: jacoco
uses: madrapps/[email protected]
with:
paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 40
min-coverage-changed-files: 60
update-comment: true
title: Test Coverage
pass-emoji: ':green_circle:'
fail-emoji: ':red_circle:'
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ out/

### Kotlin ###
.kotlin

### Secret ###
.env
src/main/resources/application*
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
# Tiketeer-Waiting
티켓팅 시스템에서 트래픽 제어를 위한 대기 큐 레포지토리

```dtd
spring:
application:
name: TiketeerWaiting
data:
redis:
repositories:
enabled: false
redis:
host:
port:
waiting:
entry-size:
```
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'

// DB
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation("it.ozimov:embedded-redis:0.7.2")

// logger
implementation 'io.github.oshai:kotlin-logging-jvm:5.1.0'
}

tasks.withType(KotlinCompile) {
Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.tiketeer.TiketeerWaiting.configuration

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.context.annotation.Profile
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory
import org.springframework.data.redis.connection.RedisConfiguration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.ReactiveRedisOperations
import org.springframework.data.redis.core.ReactiveRedisTemplate
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.RedisSerializationContext

@Configuration
@Profile("!test")
class RedisConfig {
@Value("\${spring.redis.host}")
private lateinit var host: String

@Value("\${spring.redis.port}")
private lateinit var port: Number

@Value("\${spring.redis.password}")
private lateinit var password: String

@Bean
fun redisConnectionFactory() : LettuceConnectionFactory {
val redisStandaloneConfiguration = RedisStandaloneConfiguration(host, port.toInt())
redisStandaloneConfiguration.setPassword(password)
return LettuceConnectionFactory(redisStandaloneConfiguration)
}

@Bean
fun redisTemplate(connectionFactory: ReactiveRedisConnectionFactory) : ReactiveRedisTemplate<String, String> {
return ReactiveRedisTemplate(connectionFactory, RedisSerializationContext.string())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Mono
import java.util.UUID

@RestController
Expand All @@ -15,10 +16,10 @@ class WaitingController(
private val getRankAndTokenUseCase: GetRankAndToken
) {
@GetMapping
fun getRankAndToken(@RequestParam(required = true) ticketingId: UUID): GetRankAndTokenResponseDto {
fun getRankAndToken(@RequestParam(required = true) ticketingId: UUID): Mono<GetRankAndTokenResponseDto> {
// TODO: JWT 디코딩 필터 적용 후 JWT 내에서 가져오도록 수정
val email = "[email protected]"
val result = getRankAndTokenUseCase.getRankAndToken(GetRankAndTokenCommandDto(email, ticketingId))
val result = getRankAndTokenUseCase.getRankAndToken(GetRankAndTokenCommandDto(email, ticketingId, System.currentTimeMillis()))
return GetRankAndTokenResponseDto.convertFromDto(result)
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.tiketeer.TiketeerWaiting.domain.waiting.controller.dto

import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenResultDto
import reactor.core.publisher.Mono

data class GetRankAndTokenResponseDto(
val rank: Int,
val rank: Long,
val token: String? = null,
) {
companion object {
fun convertFromDto(dto: GetRankAndTokenResultDto): GetRankAndTokenResponseDto {
return GetRankAndTokenResponseDto(dto.rank, dto.token)
fun convertFromDto(dto: Mono<GetRankAndTokenResultDto>): Mono<GetRankAndTokenResponseDto> {
return dto.flatMap { r ->
Mono.just(GetRankAndTokenResponseDto(r.rank, r.token))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package com.tiketeer.TiketeerWaiting.domain.waiting.usecase

import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenCommandDto
import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenResultDto
import reactor.core.publisher.Mono

interface GetRankAndToken {
fun getRankAndToken(dto: GetRankAndTokenCommandDto): GetRankAndTokenResultDto
fun getRankAndToken(dto: GetRankAndTokenCommandDto): Mono<GetRankAndTokenResultDto>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,40 @@ package com.tiketeer.TiketeerWaiting.domain.waiting.usecase

import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenCommandDto
import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenResultDto
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.redis.core.ReactiveRedisTemplate
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import java.util.UUID

class GetRankAndTokenUseCase: GetRankAndToken {
override fun getRankAndToken(dto: GetRankAndTokenCommandDto): GetRankAndTokenResultDto {
TODO("Not yet implemented")
@Service
class GetRankAndTokenUseCase @Autowired constructor(private val redisTemplate: ReactiveRedisTemplate<String, String>) : GetRankAndToken {
@Value("\${waiting.entry-size}")
private lateinit var entrySize: Number

override fun getRankAndToken(dto: GetRankAndTokenCommandDto): Mono<GetRankAndTokenResultDto> {
val currentTime = dto.entryTime
val token = generateToken(dto.email, dto.ticketingId, currentTime)
val mono = redisTemplate.opsForZSet().rank(dto.ticketingId.toString(), token)
.switchIfEmpty(
redisTemplate.opsForZSet().add(dto.ticketingId.toString(), token, currentTime.toDouble())
.then(redisTemplate.opsForZSet().rank(dto.ticketingId.toString(), token))
)

val ret: Mono<GetRankAndTokenResultDto> = mono
.flatMap { l ->
if (l < entrySize.toInt()) {
Mono.just(GetRankAndTokenResultDto(l, generateToken(dto.email, dto.ticketingId, currentTime)))
} else {
Mono.just(GetRankAndTokenResultDto(l))
}
}

return ret
}

private fun generateToken(email: String, ticketingId: UUID, entryTime: Long) : String {
return "${email}:${ticketingId}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import java.util.UUID

data class GetRankAndTokenCommandDto(
val email: String,
val ticketingId: UUID
val ticketingId: UUID,
val entryTime: Long
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto

data class GetRankAndTokenResultDto(
val rank: Int,
val rank: Long,
val token: String? = null,
)
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.tiketeer.TiketeerWaiting.configuration

import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import org.junit.jupiter.api.DisplayName
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory
import org.springframework.data.redis.connection.RedisStandaloneConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.ReactiveRedisTemplate
import org.springframework.data.redis.serializer.RedisSerializationContext
import redis.embedded.RedisServer
import java.io.IOException

private val logger = KotlinLogging.logger {}

@DisplayName("Embedded Redis 설정")
@TestConfiguration
class EmbeddedRedisConfig {
@Value("\${spring.data.redis.host}")
private lateinit var host: String

@Value("\${spring.data.redis.port}")
private lateinit var port: Number

@Value("\${spring.redis.maxmemory}")
private lateinit var maxmemorySize: Number

private lateinit var redisServer: RedisServer

@PostConstruct
@Throws(IOException::class)
fun startRedis() {
this.redisServer = RedisServer.builder().port(port.toInt()).setting("maxmemory " + maxmemorySize + "M").build()
try {
this.redisServer.start()
logger.info {"레디스 서버 시작 성공" }
} catch (e: Exception) {
logger.error (e) { "레디스 서버 시작 실패: ${e.message}" }
}
}

@PreDestroy
fun stopRedis() {
redisServer.stop()
}

@Bean
fun redisConnectionFactory() : LettuceConnectionFactory {
val redisStandaloneConfiguration = RedisStandaloneConfiguration(host, port.toInt())
return LettuceConnectionFactory(redisStandaloneConfiguration)
}

@Bean
fun redisTemplate(connectionFactory: ReactiveRedisConnectionFactory) : ReactiveRedisTemplate<String, String> {
return ReactiveRedisTemplate(connectionFactory, RedisSerializationContext.string())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.tiketeer.TiketeerWaiting.domain.waiting.usecase

import com.tiketeer.TiketeerWaiting.configuration.EmbeddedRedisConfig
import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenCommandDto
import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenResultDto
import org.junit.jupiter.api.BeforeEach

import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory
import reactor.test.StepVerifier
import java.util.UUID

@Import(EmbeddedRedisConfig::class)
@SpringBootTest
class GetRankAndTokenUseCaseTest {
@Autowired
lateinit var getRankAndTokenUseCase: GetRankAndTokenUseCase

@Autowired
lateinit var redisConnectionFactory: ReactiveRedisConnectionFactory

@Value("\${waiting.entry-size}")
lateinit var entrySize: Number

@BeforeEach
fun init() {
val flushDb = redisConnectionFactory.reactiveConnection.serverCommands().flushDb()
flushDb.block()
}

@Test
fun `유저 정보 생성 - 빈 대기열에 요청 - 결과 검증`() {
val email = "[email protected]"
val ticketingId = UUID.randomUUID()
val entryTime = System.currentTimeMillis()
val result = getRankAndTokenUseCase.getRankAndToken(GetRankAndTokenCommandDto(email, ticketingId, entryTime))

StepVerifier.create(result)
.expectNext(GetRankAndTokenResultDto(0, "${email}:${ticketingId}"))
.expectComplete()
.verify()
}

@Test
fun `대기열 길이만큼 유저 생성 - 대기열을 모두 채우도록 요청 후 한 명 더 요청 - 빈 토큰 결과 반환`() {
val ticketingId = UUID.randomUUID()
for (i in 1..entrySize.toInt()) {
val email = "test${i}@test.com"
val entryTime = System.currentTimeMillis()
val result = getRankAndTokenUseCase.getRankAndToken(GetRankAndTokenCommandDto(email, ticketingId, entryTime))
result.block()
}

val email = "[email protected]"
val entryTime = System.currentTimeMillis()
val result = getRankAndTokenUseCase.getRankAndToken(GetRankAndTokenCommandDto(email, ticketingId, entryTime))

StepVerifier.create(result)
.expectNext(GetRankAndTokenResultDto(entrySize.toLong()))
.expectComplete()
.verify()
}
}
Loading

0 comments on commit 70dee31

Please sign in to comment.