Skip to content

Commit

Permalink
Merge branch 'refs/heads/feature/DRAW-375' into feature/DRAW-408
Browse files Browse the repository at this point in the history
  • Loading branch information
SunwoongH committed Oct 14, 2024
2 parents d036b9e + 8614bed commit 3aaf9aa
Show file tree
Hide file tree
Showing 17 changed files with 385 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal class RedisLockAdapter(
override fun lock(key: String) {
while (getLock(key).not()) {
try {
Thread.sleep(50)
Thread.sleep(SLEEP_TIME)
} catch (e: InterruptedException) {
throw UnSupportedException
}
Expand All @@ -28,10 +28,12 @@ internal class RedisLockAdapter(
private fun getLock(key: String): Boolean {
return redisTemplate
.opsForValue()
.setIfAbsent(key + LOCK, LOCK, Duration.ofSeconds(1)) ?: throw NotFoundLockKeyException
.setIfAbsent(key + LOCK, LOCK, Duration.ofSeconds(LOCK_TIME)) ?: throw NotFoundLockKeyException
}

companion object {
private const val LOCK = "lock"
private const val LOCK_TIME = 1L
private const val SLEEP_TIME = 50L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.handler.TextWebSocketHandler

@Deprecated("com.xorker.draw.websocket.handler.RoomWebSocketHandler 사용")
@Component
internal class MainWebSocketHandler(
private val sessionManager: SessionManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal class WebSocketController(
MDC.put("roomId", request.roomId)

if (request.roomId == null) {
userConnectionUseCase.connectUser(sessionDto.user, null, request.locale)
userConnectionUseCase.connectUserOld(sessionDto.user, null, request.locale)
return
}

Expand All @@ -60,7 +60,7 @@ internal class WebSocketController(
throw MaxRoomException
}

userConnectionUseCase.connectUser(sessionDto.user, roomId, request.locale)
userConnectionUseCase.connectUserOld(sessionDto.user, roomId, request.locale)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.xorker.draw.websocket.message.request.WebSocketRequest
import com.xorker.draw.websocket.message.request.mafia.MafiaGameInferAnswerRequest
import com.xorker.draw.websocket.message.request.mafia.MafiaGameReactionRequest
import com.xorker.draw.websocket.message.request.mafia.MafiaGameVoteMafiaRequest
import com.xorker.draw.websocket.session.Session
import com.xorker.draw.websocket.session.SessionId
import com.xorker.draw.websocket.session.SessionManager
import org.springframework.stereotype.Component
Expand All @@ -24,6 +25,35 @@ internal class WebSocketRouter(
private val mafiaPhaseUseCase: MafiaPhaseUseCase,
private val mafiaGameUseCase: MafiaGameUseCase,
) {
fun route(session: Session, request: WebSocketRequest) {
when (request.action) {
RequestAction.PING -> sessionManager.setPing(session.id)
RequestAction.INIT -> throw InvalidRequestValueException
RequestAction.RANDOM_MATCHING -> throw InvalidRequestValueException
RequestAction.START_GAME -> mafiaPhaseUseCase.startGame(session.user)
RequestAction.DRAW -> mafiaGameUseCase.draw(session.user, request.extractBody())
RequestAction.END_TURN -> mafiaGameUseCase.nextTurnByUser(session.user)
RequestAction.VOTE -> {
val parsedRequest = request.extractBody<MafiaGameVoteMafiaRequest>()
mafiaGameUseCase.voteMafia(session.user, UserId(parsedRequest.userId))
}

RequestAction.ANSWER -> {
val parsedRequest = request.extractBody<MafiaGameInferAnswerRequest>()
mafiaGameUseCase.inferAnswer(session.user, parsedRequest.answer)
}

RequestAction.DECIDE_ANSWER -> {
val parsedRequest = request.extractBody<MafiaGameInferAnswerRequest>()
mafiaGameUseCase.decideAnswer(session.user, parsedRequest.answer)
}

RequestAction.REACTION -> {
val parsedRequest = request.extractBody<MafiaGameReactionRequest>()
mafiaGameUseCase.react(session.user, parsedRequest.reaction)
}
}
}

fun route(session: WebSocketSession, request: WebSocketRequest) {
if (request.action == RequestAction.PING) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.xorker.draw.websocket.config

import com.xorker.draw.websocket.MainWebSocketHandler
import com.xorker.draw.websocket.handler.QuickWebSocketHandler
import com.xorker.draw.websocket.handler.RoomWebSocketHandler
import org.springframework.context.annotation.Configuration
import org.springframework.web.socket.config.annotation.EnableWebSocket
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
Expand All @@ -10,10 +12,15 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@Configuration
internal class WebSocketConfig(
private val handler: MainWebSocketHandler,
private val roomWebSocketHandler: RoomWebSocketHandler,
private val quickWebSocketHandler: QuickWebSocketHandler,
) : WebSocketConfigurer {

override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(handler, "/trouble-painter")
registry
.addHandler(handler, "/trouble-painter")
.addHandler(roomWebSocketHandler, "/mafia/room")
.addHandler(quickWebSocketHandler, "/mafia/quick")
.setAllowedOrigins("*")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package com.xorker.draw.websocket.handler

import com.fasterxml.jackson.databind.ObjectMapper
import com.xorker.draw.auth.token.TokenUseCase
import com.xorker.draw.exception.InvalidRequestValueException
import com.xorker.draw.exception.UnAuthenticationException
import com.xorker.draw.mafia.MafiaGameUseCase
import com.xorker.draw.support.logging.defaultApiJsonMap
import com.xorker.draw.support.logging.logger
import com.xorker.draw.support.logging.registerRequestId
import com.xorker.draw.user.User
import com.xorker.draw.user.UserId
import com.xorker.draw.websocket.message.request.RequestAction
import com.xorker.draw.websocket.message.request.WebSocketRequest
import com.xorker.draw.websocket.message.request.WebSocketRequestParser
import com.xorker.draw.websocket.message.request.mafia.SessionInitializeRequest
import com.xorker.draw.websocket.session.Session
import com.xorker.draw.websocket.session.SessionId
import com.xorker.draw.websocket.session.SessionManager
import com.xorker.draw.websocket.session.SessionWrapper
import java.net.URLDecoder
import java.nio.charset.StandardCharsets
import org.slf4j.MDC
import org.springframework.web.socket.CloseStatus
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.handler.TextWebSocketHandler

internal abstract class BaseWebSocketHandler(
private val objectMapper: ObjectMapper,
private val sessionManager: SessionManager,
private val parser: WebSocketRequestParser,
private val tokenUseCase: TokenUseCase,
private val gameUseCase: MafiaGameUseCase,
) : TextWebSocketHandler() {
private val logger = logger()

abstract fun afterConnect(session: Session)
abstract fun action(session: Session, request: WebSocketRequest)
abstract fun afterDisconnect(session: Session?, status: CloseStatus)

override fun afterConnectionEstablished(session: WebSocketSession) {
registerRequestId()
val user = getUser(session) ?: throw UnAuthenticationException
val locale = session.getHeader(HEADER_LOCALE) ?: throw InvalidRequestValueException

val sessionDto = SessionWrapper(session, user, locale)
setupMdc(sessionDto)
sessionManager.registerSession(sessionDto)

try {
afterConnect(sessionDto)
} finally {
log(sessionDto.id, "WS_CONNECT")
MDC.clear()
}
}

override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
registerRequestId()

val sessionId = SessionId(session.id)
val sessionDto = sessionManager.getSession(sessionId)

if (sessionDto == null) {
logger.error("Session이 존재하지 않습니다. ${session.id}")
return
}

setupMdc(sessionDto)

val request = parser.parse(message.payload)

try {
action(sessionDto, request)
} finally {
log(sessionId, request)
MDC.clear()
}
}

override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
registerRequestId()

val sessionId = SessionId(session.id)
val sessionDto = sessionManager.unregisterSession(sessionId)
if (sessionDto != null) {
setupMdc(sessionDto)
}

try {
afterDisconnect(sessionDto, status)
} finally {
log(
sessionId,
"WS_CLOSED",
"status" to status,
)
MDC.clear()
}
}

private fun setupMdc(session: Session) {
val gameInfo = gameUseCase.getGameInfoByUserId(session.user.id) ?: return
MDC.put("userId", session.user.id.value.toString())
MDC.put("roomId", gameInfo.room.id.value)
}

private fun log(sessionId: SessionId, action: String, vararg additionalData: Pair<String, Any>) {
val data = defaultApiJsonMap(
"action" to action,
"sessionId" to sessionId.value,
"userId" to MDC.get("userId"),
"roomId" to MDC.get("roomId"),
*additionalData,
)

val log = objectMapper.writeValueAsString(data)
logger.info(log)
}

private fun log(sessionId: SessionId, request: WebSocketRequest) {
val body: Any? = if (request.action == RequestAction.INIT) {
objectMapper.readValue(request.body, SessionInitializeRequest::class.java).copy(accessToken = "[masked]")
} else {
request.body
}

val data = defaultApiJsonMap(
"action" to request.action,
"requestBody" to body,
"sessionId" to sessionId.value,
"userId" to MDC.get("userId"),
"roomId" to MDC.get("roomId"),
)

val log = objectMapper.writeValueAsString(data)
logger.info(log)
}

private fun getUser(session: WebSocketSession): User? {
val userId = getUserId(session) ?: return null
val encodedNickname = session.getHeader(HEADER_NICKNAME) ?: return null
val nickname = URLDecoder.decode(encodedNickname, StandardCharsets.UTF_8.toString())

return User(userId, nickname)
}

private fun getUserId(session: WebSocketSession): UserId? {
val header = session.getHeader(HEADER_AUTHORIZATION) ?: return null

if (header.startsWith(HEADER_BEARER)) {
val accessToken = header.substring(HEADER_BEARER.length)

return tokenUseCase.getUserId(accessToken)
}

return null
}

companion object {
private const val HEADER_AUTHORIZATION = "Authorization"
private const val HEADER_BEARER = "bearer "
private const val HEADER_NICKNAME = "Nickname"
private const val HEADER_LOCALE = "locale"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.xorker.draw.websocket.handler

import com.fasterxml.jackson.databind.ObjectMapper
import com.xorker.draw.auth.token.TokenUseCase
import com.xorker.draw.mafia.MafiaGameUseCase
import com.xorker.draw.mafia.UserConnectionUseCase
import com.xorker.draw.mafia.WaitingQueueUseCase
import com.xorker.draw.websocket.WebSocketRouter
import com.xorker.draw.websocket.message.request.WebSocketRequest
import com.xorker.draw.websocket.message.request.WebSocketRequestParser
import com.xorker.draw.websocket.session.Session
import com.xorker.draw.websocket.session.SessionManager
import org.springframework.stereotype.Component
import org.springframework.web.socket.CloseStatus

@Component
internal class QuickWebSocketHandler(
objectMapper: ObjectMapper,
sessionManager: SessionManager,
parser: WebSocketRequestParser,
tokenUseCase: TokenUseCase,
gameUseCase: MafiaGameUseCase,
private val userConnectionUseCase: UserConnectionUseCase,
private val router: WebSocketRouter,
private val waitingQueueUseCase: WaitingQueueUseCase,
) : BaseWebSocketHandler(
objectMapper,
sessionManager,
parser,
tokenUseCase,
gameUseCase,
) {
override fun afterConnect(session: Session) {
waitingQueueUseCase.enqueue(session.user, session.locale)
}

override fun action(session: Session, request: WebSocketRequest) {
router.route(session, request)
}

override fun afterDisconnect(session: Session?, status: CloseStatus) {
val user = session?.user ?: return

waitingQueueUseCase.remove(user, session.locale)

when (status) {
CloseStatus.NORMAL -> userConnectionUseCase.exitUser(user)
else -> userConnectionUseCase.disconnectUser(user)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.xorker.draw.websocket.handler

import com.fasterxml.jackson.databind.ObjectMapper
import com.xorker.draw.auth.token.TokenUseCase
import com.xorker.draw.mafia.MafiaGameUseCase
import com.xorker.draw.mafia.UserConnectionUseCase
import com.xorker.draw.room.RoomId
import com.xorker.draw.websocket.WebSocketRouter
import com.xorker.draw.websocket.message.request.WebSocketRequest
import com.xorker.draw.websocket.message.request.WebSocketRequestParser
import com.xorker.draw.websocket.session.Session
import com.xorker.draw.websocket.session.SessionManager
import org.springframework.stereotype.Component
import org.springframework.web.socket.CloseStatus

@Component
internal class RoomWebSocketHandler(
objectMapper: ObjectMapper,
sessionManager: SessionManager,
parser: WebSocketRequestParser,
tokenUseCase: TokenUseCase,
gameUseCase: MafiaGameUseCase,
private val userConnectionUseCase: UserConnectionUseCase,
private val router: WebSocketRouter,
) : BaseWebSocketHandler(
objectMapper,
sessionManager,
parser,
tokenUseCase,
gameUseCase,
) {
override fun afterConnect(session: Session) {
userConnectionUseCase.connectUser(
user = session.user,
roomId = RoomId(session.origin.getHeader(HEADER_ROOM_ID)),
locale = session.locale,
)
}

override fun action(session: Session, request: WebSocketRequest) {
router.route(session, request)
}

override fun afterDisconnect(session: Session?, status: CloseStatus) {
val user = session?.user ?: return

when (status) {
CloseStatus.NORMAL -> userConnectionUseCase.exitUser(user)
else -> userConnectionUseCase.disconnectUser(user)
}
}

companion object {
private const val HEADER_ROOM_ID = "roomId"
}
}
Loading

0 comments on commit 3aaf9aa

Please sign in to comment.