Skip to content

Commit

Permalink
Merge pull request #107 from soma-baekgu/feature/BG-387-ws-connection…
Browse files Browse the repository at this point in the history
…-limit

[BG-387]: 유저 웹 소켓 연결 커넥션 수 제한 (4h / 2h)
  • Loading branch information
GGHDMS authored Aug 30, 2024
2 parents d3cf3a2 + a8c9cf5 commit 321592d
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ package com.backgu.amaker.realtime.common.excpetion
import com.backgu.amaker.common.status.StatusCode

class RealTimeException(
val statusCode: StatusCode,
statusCode: StatusCode,
) : RuntimeException(statusCode.message)
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.backgu.amaker.realtime.config

import com.backgu.amaker.infra.redis.session.SessionRedisData
import com.backgu.amaker.realtime.server.config.ServerConfig
import com.backgu.amaker.realtime.session.service.SessionDeleteSubscriber
import io.lettuce.core.SocketOptions
import io.lettuce.core.cluster.ClusterClientOptions
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions
Expand All @@ -14,6 +16,9 @@ import org.springframework.data.redis.connection.RedisPassword
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.listener.ChannelTopic
import org.springframework.data.redis.listener.RedisMessageListenerContainer
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer
Expand Down Expand Up @@ -74,4 +79,22 @@ class ProdRedisConfig(
template.valueSerializer = GenericJackson2JsonRedisSerializer()
return template
}

@Bean
fun messageListenerAdapter(sessionDeleteSubscriber: SessionDeleteSubscriber): MessageListenerAdapter =
MessageListenerAdapter(sessionDeleteSubscriber, "dropOutSessions")

@Bean
fun redisContainer(
messageListenerAdapter: MessageListenerAdapter,
topic: ChannelTopic,
): RedisMessageListenerContainer {
val container = RedisMessageListenerContainer()
container.setConnectionFactory(redisConnectionFactory())
container.addMessageListener(messageListenerAdapter, topic)
return container
}

@Bean
fun topic(serverConfig: ServerConfig): ChannelTopic = ChannelTopic(serverConfig.id)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
package com.backgu.amaker.realtime.config

import com.backgu.amaker.infra.redis.session.SessionRedisData
import com.backgu.amaker.realtime.server.config.ServerConfig
import com.backgu.amaker.realtime.session.service.SessionDeleteSubscriber
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.listener.ChannelTopic
import org.springframework.data.redis.listener.RedisMessageListenerContainer
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer

Expand All @@ -33,4 +38,22 @@ class RedisConfig {
template.valueSerializer = GenericJackson2JsonRedisSerializer()
return template
}

@Bean
fun messageListenerAdapter(sessionDeleteSubscriber: SessionDeleteSubscriber): MessageListenerAdapter =
MessageListenerAdapter(sessionDeleteSubscriber, "dropOutSessions")

@Bean
fun redisContainer(
messageListenerAdapter: MessageListenerAdapter,
topic: ChannelTopic,
): RedisMessageListenerContainer {
val container = RedisMessageListenerContainer()
container.setConnectionFactory(redisConnectionFactory())
container.addMessageListener(messageListenerAdapter, topic)
return container
}

@Bean
fun topic(serverConfig: ServerConfig): ChannelTopic = ChannelTopic(serverConfig.id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.backgu.amaker.realtime.session.service

import com.backgu.amaker.infra.redis.session.SessionRedisData
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service

@Service
class SessionDeletePublisher(
private val redisTemplate: RedisTemplate<String, SessionRedisData>,
) {
fun publish(
serverId: String,
sessionRedisData: SessionRedisData,
) {
redisTemplate.convertAndSend(serverId, sessionRedisData)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.backgu.amaker.realtime.session.service

import com.backgu.amaker.infra.redis.session.SessionRedisData
import com.backgu.amaker.realtime.user.service.UserSessionService
import com.backgu.amaker.realtime.workspace.service.WorkspaceSessionService
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.stereotype.Service

@Service
class SessionDeleteSubscriber(
private val workspaceSessionService: WorkspaceSessionService,
private val userSessionService: UserSessionService,
private val objectMapper: ObjectMapper,
) {
fun dropOutSessions(message: String) {
val sessionRedisData = objectMapper.readValue(message, SessionRedisData::class.java)
workspaceSessionService.dropOut(sessionRedisData.workspaceId, sessionRedisData.toDomain())
userSessionService.dropOut(sessionRedisData.userId, sessionRedisData.toDomain())
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.backgu.amaker.realtime.session.service

import com.backgu.amaker.application.workspace.WorkspaceUserService
import com.backgu.amaker.infra.redis.session.SessionRedisData
import com.backgu.amaker.realtime.server.config.ServerConfig
import com.backgu.amaker.realtime.session.session.RealTimeSession
import com.backgu.amaker.realtime.user.service.UserSessionService
import com.backgu.amaker.realtime.workspace.service.WorkspaceSessionService
Expand All @@ -12,13 +14,25 @@ class SessionFacadeService(
private val workspaceUserService: WorkspaceUserService,
private val workspaceSessionService: WorkspaceSessionService,
private val userSessionService: UserSessionService,
private val sessionDeletePublisher: SessionDeletePublisher,
private val serverConfig: ServerConfig,
) {
fun enrollUserToWorkspaceSession(
userId: String,
workspaceId: Long,
workspaceRealTimeSession: RealTimeSession<WebSocketSession>,
) {
workspaceUserService.validUserInWorkspace(userId, workspaceId)

workspaceSessionService.findDropOutSessionIfLimit(workspaceId, userId).forEach {
if (it.realtimeId != serverConfig.id) {
sessionDeletePublisher.publish(it.realtimeId, SessionRedisData.of(it))
} else {
workspaceSessionService.dropOut(workspaceId, it)
userSessionService.dropOut(userId, it)
}
}

workspaceSessionService.enrollUserToWorkspaceSession(workspaceId, workspaceRealTimeSession)
userSessionService.enrollUserToUserSession(userId, workspaceRealTimeSession)
}
Expand All @@ -28,7 +42,7 @@ class SessionFacadeService(
workspaceId: Long,
workspaceRealTimeSession: RealTimeSession<WebSocketSession>,
) {
userSessionService.dropOut(userId, workspaceRealTimeSession)
workspaceSessionService.dropOut(workspaceId, workspaceRealTimeSession)
userSessionService.dropOut(userId, workspaceRealTimeSession)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,9 @@ class RealTimeSession<T>(
val realTimeId: String,
val session: T,
) {
companion object {
const val WORKSPACE_USER_SESSION_LIMIT = 20
}

fun toDomain() = Session(id, userId, workspaceId, realTimeId)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.backgu.amaker.realtime.user.service

import com.backgu.amaker.domain.session.Session
import com.backgu.amaker.infra.redis.session.user.repository.UserSessionRepository
import com.backgu.amaker.realtime.server.config.ServerConfig
import com.backgu.amaker.realtime.session.session.RealTimeSession
Expand Down Expand Up @@ -49,4 +50,15 @@ class UserSessionService(
)
sessionStorage.removeSession(realTimeSession.id)
}

fun dropOut(
userId: String,
session: Session,
) {
userSessionRepository.removeUserSession(
userId,
session,
)
sessionStorage.removeSession(session.id)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.backgu.amaker.realtime.workspace.service

import com.backgu.amaker.domain.session.Session
import com.backgu.amaker.infra.redis.session.workspace.repository.WorkspaceSessionRepository
import com.backgu.amaker.realtime.server.config.ServerConfig
import com.backgu.amaker.realtime.session.session.RealTimeSession
Expand Down Expand Up @@ -32,6 +33,14 @@ class WorkspaceSessionService(
sessionStorage.removeSession(session.id)
}

fun dropOut(
workspaceId: Long,
session: Session,
) {
workspaceSessionRepository.removeWorkspaceSession(workspaceId, session)
sessionStorage.removeSession(session.id)
}

fun getWorkspaceSession(workspaceId: Long): List<RealTimeSession<WebSocketSession>> {
val workspaceSessions =
workspaceSessionRepository
Expand All @@ -41,4 +50,21 @@ class WorkspaceSessionService(

return sessionStorage.getSessions(workspaceSessions.map { it.id })
}

fun findDropOutSessionIfLimit(
workspaceId: Long,
userId: String,
): List<Session> {
val workspaceSessions =
workspaceSessionRepository
.findWorkspaceSessionByWorkspaceId(workspaceId)
.map { it.toDomain() }
.filter { it.userId == userId }

if (workspaceSessions.size >= RealTimeSession.WORKSPACE_USER_SESSION_LIMIT) {
return workspaceSessions.drop(RealTimeSession.WORKSPACE_USER_SESSION_LIMIT - 1)
}

return emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.backgu.amaker.common.status.StatusCode
import com.backgu.amaker.realtime.common.excpetion.RealTimeException
import com.backgu.amaker.realtime.session.session.RealTimeSession
import org.springframework.stereotype.Component
import org.springframework.web.socket.CloseStatus
import org.springframework.web.socket.WebSocketSession
import java.util.concurrent.ConcurrentHashMap

Expand All @@ -16,7 +17,10 @@ class SessionStorage {
}

fun removeSession(id: String) {
sessionsMap.remove(id)
sessionsMap[id]?.let {
it.session.close(CloseStatus.NORMAL)
sessionsMap.remove(id)
}
}

fun getSessions(ids: Collection<String>): List<RealTimeSession<WebSocketSession>> = ids.mapNotNull { sessionsMap[it] }
Expand Down

0 comments on commit 321592d

Please sign in to comment.