Merge pull request #118 from dalbodeule/debug

debug on WSSongListRoutes.kt
This commit is contained in:
JinU Choi 2024-09-24 11:28:58 +09:00 committed by GitHub
commit 99686496b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 162 additions and 121 deletions

View File

@ -7,7 +7,8 @@ enum class SongType(var value: Int) {
REMOVE(1), REMOVE(1),
NEXT(2), NEXT(2),
STREAM_OFF(50) STREAM_OFF(50),
ACK(51)
} }
class SongEvent( class SongEvent(

View File

@ -12,7 +12,7 @@ object SongLists: IntIdTable("song_list") {
val uid = varchar("uid", 64) val uid = varchar("uid", 64)
val url = varchar("url", 128) val url = varchar("url", 128)
val name = text("name") val name = text("name")
val reqName = varchar("req_name", 20) val reqName = varchar("req_name", 80)
val author = text("author") val author = text("author")
val time = integer("time") val time = integer("time")
val created_at = datetime("created_at").default(LocalDateTime.now()) val created_at = datetime("created_at").default(LocalDateTime.now())

View File

@ -24,9 +24,8 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.webserver.routes.* import space.mori.chzzk_bot.webserver.routes.*
import space.mori.chzzk_bot.webserver.utils.DiscordGuildCache
import space.mori.chzzk_bot.webserver.utils.DiscordRatelimits import space.mori.chzzk_bot.webserver.utils.DiscordRatelimits
import space.mori.chzzk_bot.webserver.utils.Guild import wsSongListRoutes
import java.time.Duration import java.time.Duration
val dotenv = dotenv { val dotenv = dotenv {

View File

@ -69,7 +69,9 @@ fun Routing.apiCommandRoutes() {
commandRequest.failContent ?: "" commandRequest.failContent ?: ""
) )
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(CommandReloadEvent(user.token ?: "")) for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token ?: ""))
}
} }
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} }
@ -104,7 +106,9 @@ fun Routing.apiCommandRoutes() {
commandRequest.failContent ?: "" commandRequest.failContent ?: ""
) )
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(CommandReloadEvent(user.token ?: "")) for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token ?: ""))
}
} }
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} catch(e: Exception) { } catch(e: Exception) {
@ -137,7 +141,9 @@ fun Routing.apiCommandRoutes() {
try { try {
CommandService.removeCommand(user, commandRequest.label) CommandService.removeCommand(user, commandRequest.label)
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
dispatcher.post(CommandReloadEvent(user.token ?: "")) for(i: Int in 0..3) {
dispatcher.post(CommandReloadEvent(user.token ?: ""))
}
} }
call.respond(HttpStatusCode.OK) call.respond(HttpStatusCode.OK)
} catch(e: Exception) { } catch(e: Exception) {

View File

@ -1,9 +1,9 @@
package space.mori.chzzk_bot.webserver.routes
import io.ktor.server.routing.* import io.ktor.server.routing.*
import io.ktor.server.sessions.* import io.ktor.server.sessions.*
import io.ktor.server.websocket.* import io.ktor.server.websocket.*
import io.ktor.util.logging.Logger
import io.ktor.websocket.* import io.ktor.websocket.*
import io.ktor.websocket.Frame.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.channels.ClosedReceiveChannelException
@ -15,12 +15,16 @@ import org.koin.java.KoinJavaComponent.inject
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import space.mori.chzzk_bot.common.events.* import space.mori.chzzk_bot.common.events.*
import space.mori.chzzk_bot.common.models.SongList import space.mori.chzzk_bot.common.models.SongList
import space.mori.chzzk_bot.common.models.SongLists.uid
import space.mori.chzzk_bot.common.models.User
import space.mori.chzzk_bot.common.services.SongConfigService import space.mori.chzzk_bot.common.services.SongConfigService
import space.mori.chzzk_bot.common.services.SongListService import space.mori.chzzk_bot.common.services.SongListService
import space.mori.chzzk_bot.common.services.UserService import space.mori.chzzk_bot.common.services.UserService
import space.mori.chzzk_bot.common.utils.YoutubeVideo import space.mori.chzzk_bot.common.utils.YoutubeVideo
import space.mori.chzzk_bot.common.utils.getYoutubeVideo import space.mori.chzzk_bot.common.utils.getYoutubeVideo
import space.mori.chzzk_bot.webserver.UserSession import space.mori.chzzk_bot.webserver.UserSession
import space.mori.chzzk_bot.webserver.routes.SongResponse
import space.mori.chzzk_bot.webserver.routes.toSerializable
import space.mori.chzzk_bot.webserver.utils.CurrentSong import space.mori.chzzk_bot.webserver.utils.CurrentSong
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -32,12 +36,11 @@ fun Routing.wsSongListRoutes() {
val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java) val dispatcher: CoroutinesEventBus by inject(CoroutinesEventBus::class.java)
fun addSession(uid: String, session: WebSocketServerSession) { fun addSession(uid: String, session: WebSocketServerSession) {
if(sessions[uid] != null) { if (sessions[uid] != null) {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
sessions[uid]?.close( sessions[uid]?.close(
CloseReason(CloseReason.Codes.VIOLATED_POLICY, CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Duplicated sessions.")
"Duplicated sessions." )
))
} }
} }
sessions[uid] = session sessions[uid] = session
@ -47,6 +50,24 @@ fun Routing.wsSongListRoutes() {
sessions.remove(uid) sessions.remove(uid)
} }
suspend fun waitForAck(ws: WebSocketServerSession, expectedType: Int): Boolean {
val timeout = 5000L // 5 seconds timeout
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
for (frame in ws.incoming) {
if (frame is Text) {
val message = frame.readText()
val data = Json.decodeFromString<SongRequest>(message)
if (data.type == SongType.ACK.value) {
return true // ACK received
}
}
}
delay(100) // Check every 100 ms
}
return false // Timeoutㅌ
}
suspend fun sendWithRetry(uid: String, res: SongResponse, maxRetries: Int = 5, delayMillis: Long = 3000L) { suspend fun sendWithRetry(uid: String, res: SongResponse, maxRetries: Int = 5, delayMillis: Long = 3000L) {
var attempt = 0 var attempt = 0
var sentSuccessfully = false var sentSuccessfully = false
@ -56,11 +77,17 @@ fun Routing.wsSongListRoutes() {
try { try {
// Attempt to send the message // Attempt to send the message
ws?.sendSerialized(res) ws?.sendSerialized(res)
sentSuccessfully = true // If no exception, mark as sent successfully logger.debug("Message sent successfully to $uid on attempt $attempt")
logger.debug("Message sent successfully on attempt $attempt") // Wait for ACK
val ackReceived = waitForAck(ws!!, res.type)
if (ackReceived) {
sentSuccessfully = true
} else {
logger.warn("ACK not received for message to $uid on attempt $attempt.")
}
} catch (e: Exception) { } catch (e: Exception) {
attempt++ attempt++
logger.warn("Failed to send message on attempt $attempt. Retrying in $delayMillis ms.") logger.warn("Failed to send message to $uid on attempt $attempt. Retrying in $delayMillis ms.")
logger.warn(e.stackTraceToString()) logger.warn(e.stackTraceToString())
// Wait before retrying // Wait before retrying
@ -69,13 +96,13 @@ fun Routing.wsSongListRoutes() {
} }
if (!sentSuccessfully) { if (!sentSuccessfully) {
logger.error("Failed to send message after $maxRetries attempts.") logger.error("Failed to send message to $uid after $maxRetries attempts.")
} }
} }
webSocket("/songlist") { webSocket("/songlist") {
val session = call.sessions.get<UserSession>() val session = call.sessions.get<UserSession>()
val user = session?.id?.let { UserService.getUserWithNaverId( it ) } val user = session?.id?.let { UserService.getUserWithNaverId(it) }
if (user == null) { if (user == null) {
close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID")) close(CloseReason(CloseReason.Codes.CANNOT_ACCEPT, "Invalid SID"))
return@webSocket return@webSocket
@ -85,7 +112,7 @@ fun Routing.wsSongListRoutes() {
addSession(uid!!, this) addSession(uid!!, this)
if(status[uid] == SongType.STREAM_OFF) { if (status[uid] == SongType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
sendSerialized(SongResponse( sendSerialized(SongResponse(
SongType.STREAM_OFF.value, SongType.STREAM_OFF.value,
@ -100,111 +127,22 @@ fun Routing.wsSongListRoutes() {
try { try {
for (frame in incoming) { for (frame in incoming) {
when(frame) { when (frame) {
is Frame.Text -> { is Text -> {
if (frame.readText() == "ping") { if (frame.readText() == "ping") {
send("pong") send("pong")
} else { } else {
val data = frame.readText().let { Json.decodeFromString<SongRequest>(it) } val data = frame.readText().let { Json.decodeFromString<SongRequest>(it) }
if (data.maxQueue != null && data.maxQueue > 0) SongConfigService.updateQueueLimit( // Handle song requests
user, handleSongRequest(data, user, dispatcher, logger)
data.maxQueue
)
if (data.maxUserLimit != null && data.maxUserLimit > 0) SongConfigService.updatePersonalLimit(
user,
data.maxUserLimit
)
if (data.isStreamerOnly != null) SongConfigService.updateStreamerOnly(
user,
data.isStreamerOnly
)
if (data.isDisabled != null) SongConfigService.updateDisabled(user, data.isDisabled)
if (data.type == SongType.ADD.value && data.url != null) {
try {
val youtubeVideo = getYoutubeVideo(data.url)
if (youtubeVideo != null) {
CoroutineScope(Dispatchers.Default).launch {
SongListService.saveSong(
user,
user.token!!,
data.url,
youtubeVideo.name,
youtubeVideo.author,
youtubeVideo.length,
user.username
)
dispatcher.post(
SongEvent(
user.token!!,
SongType.ADD,
user.token,
CurrentSong.getSong(user),
youtubeVideo
)
)
}
}
} catch (e: Exception) {
logger.debug("SongType.ADD Error: $uid $e")
}
} else if (data.type == SongType.REMOVE.value && data.url != null) {
val songs = SongListService.getSong(user)
val exactSong = songs.firstOrNull { it.url == data.url }
if (exactSong != null) {
SongListService.deleteSong(user, exactSong.uid, exactSong.name)
}
dispatcher.post(
SongEvent(
user.token!!,
SongType.REMOVE,
null,
null,
null,
data.url
)
)
} else if (data.type == SongType.NEXT.value) {
val songList = SongListService.getSong(user)
var song: SongList? = null
var youtubeVideo: YoutubeVideo? = null
if (songList.isNotEmpty()) {
song = songList[0]
SongListService.deleteSong(user, song.uid, song.name)
}
song?.let {
youtubeVideo = YoutubeVideo(
song.url,
song.name,
song.author,
song.time
)
}
dispatcher.post(
SongEvent(
user.token!!,
SongType.NEXT,
song?.uid,
youtubeVideo
)
)
CurrentSong.setSong(user, youtubeVideo)
}
} }
} }
is Frame.Ping -> send(Frame.Pong(frame.data)) is Ping -> send(Pong(frame.data))
else -> { else -> ""
}
} }
} }
} catch(e: ClosedReceiveChannelException) { } catch (e: ClosedReceiveChannelException) {
logger.error("Error in WebSocket: ${e.message}") logger.error("Error in WebSocket: ${e.message}")
} finally { } finally {
removeSession(uid) removeSession(uid)
@ -215,7 +153,7 @@ fun Routing.wsSongListRoutes() {
logger.debug("SongEvent: {} / {} {}", it.uid, it.type, it.current?.name) logger.debug("SongEvent: {} / {} {}", it.uid, it.type, it.current?.name)
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
val user = UserService.getUser(it.uid) val user = UserService.getUser(it.uid)
if(user != null) { if (user != null) {
user.token?.let { token -> user.token?.let { token ->
sendWithRetry( sendWithRetry(
token, SongResponse( token, SongResponse(
@ -225,16 +163,18 @@ fun Routing.wsSongListRoutes() {
it.current?.toSerializable(), it.current?.toSerializable(),
it.next?.toSerializable(), it.next?.toSerializable(),
it.delUrl it.delUrl
)) )
)
} }
} }
} }
} }
dispatcher.subscribe(TimerEvent::class) { dispatcher.subscribe(TimerEvent::class) {
if(it.type == TimerType.STREAM_OFF) { if (it.type == TimerType.STREAM_OFF) {
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
val user = UserService.getUser(it.uid) val user = UserService.getUser(it.uid)
if(user != null) { if (user != null) {
user.token?.let { token -> user.token?.let { token ->
sendWithRetry( sendWithRetry(
token, SongResponse( token, SongResponse(
@ -243,7 +183,8 @@ fun Routing.wsSongListRoutes() {
null, null,
null, null,
null, null,
)) )
)
} }
} }
} }
@ -251,6 +192,100 @@ fun Routing.wsSongListRoutes() {
} }
} }
suspend fun handleSongRequest(
data: SongRequest,
user: User,
dispatcher: CoroutinesEventBus,
logger: Logger
) {
if (data.maxQueue != null && data.maxQueue > 0) SongConfigService.updateQueueLimit(user, data.maxQueue)
if (data.maxUserLimit != null && data.maxUserLimit > 0) SongConfigService.updatePersonalLimit(user, data.maxUserLimit)
if (data.isStreamerOnly != null) SongConfigService.updateStreamerOnly(user, data.isStreamerOnly)
if (data.isDisabled != null) SongConfigService.updateDisabled(user, data.isDisabled)
when (data.type) {
SongType.ADD.value -> {
data.url?.let { url ->
try {
val youtubeVideo = getYoutubeVideo(url)
if (youtubeVideo != null) {
CoroutineScope(Dispatchers.Default).launch {
SongListService.saveSong(
user,
user.token!!,
url,
youtubeVideo.name,
youtubeVideo.author,
youtubeVideo.length,
user.username
)
dispatcher.post(
SongEvent(
user.token!!,
SongType.ADD,
user.token,
CurrentSong.getSong(user),
youtubeVideo
)
)
}
}
} catch (e: Exception) {
logger.debug("SongType.ADD Error: $uid $e")
}
}
}
SongType.REMOVE.value -> {
data.url?.let { url ->
val songs = SongListService.getSong(user)
val exactSong = songs.firstOrNull { it.url == url }
if (exactSong != null) {
SongListService.deleteSong(user, exactSong.uid, exactSong.name)
}
dispatcher.post(
SongEvent(
user.token!!,
SongType.REMOVE,
null,
null,
null,
url
)
)
}
}
SongType.NEXT.value -> {
val songList = SongListService.getSong(user)
var song: SongList? = null
var youtubeVideo: YoutubeVideo? = null
if (songList.isNotEmpty()) {
song = songList[0]
SongListService.deleteSong(user, song.uid, song.name)
}
song?.let {
youtubeVideo = YoutubeVideo(
song.url,
song.name,
song.author,
song.time
)
}
dispatcher.post(
SongEvent(
user.token!!,
SongType.NEXT,
song?.uid,
youtubeVideo
)
)
CurrentSong.setSong(user, youtubeVideo)
}
}
}
@Serializable @Serializable
data class SongRequest( data class SongRequest(
val type: Int, val type: Int,
@ -261,4 +296,4 @@ data class SongRequest(
val isStreamerOnly: Boolean?, val isStreamerOnly: Boolean?,
val remove: Int?, val remove: Int?,
val isDisabled: Boolean?, val isDisabled: Boolean?,
) )