diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 9856e3ee..94dbf8a4 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -46,7 +46,7 @@ tasks.getByName("bootJar") { } tasks.getByName("jar") { - enabled = false + enabled = true } springBoot.buildInfo { properties { } } diff --git a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt index 641a8f77..692c736c 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt @@ -1,5 +1,8 @@ package com.oksusu.susu.api.event.listener +import com.oksusu.susu.api.auth.application.AuthFacade +import com.oksusu.susu.api.auth.model.AuthUser +import com.oksusu.susu.api.auth.model.AuthUserToken import com.oksusu.susu.api.common.aspect.SusuEventListener import com.oksusu.susu.api.event.model.SystemActionLogEvent import com.oksusu.susu.api.log.application.SystemActionLogService @@ -9,27 +12,37 @@ import com.oksusu.susu.domain.log.domain.SystemActionLog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import kotlinx.coroutines.reactor.awaitSingleOrNull import org.springframework.context.event.EventListener +import reactor.kotlin.core.publisher.toMono @SusuEventListener class SystemActionLogEventListener( private val systemActionLogService: SystemActionLogService, private val coroutineExceptionHandler: ErrorPublishingCoroutineExceptionHandler, + private val authFacade: AuthFacade, ) { @EventListener fun subscribe(event: SystemActionLogEvent) { + if (check(event)) { + return + } + mdcCoroutineScope(Dispatchers.IO + Job() + coroutineExceptionHandler.handler, event.traceId).launch { - if (check(event)) { - SystemActionLog( - ipAddress = event.ipAddress, - path = event.path, - httpMethod = event.method, - userAgent = event.userAgent, - host = event.host, - referer = event.referer, - extra = event.extra - ).run { systemActionLogService.record(this) } - } + val authUser = event.token + ?.let { token -> AuthUserToken.from(token).toMono() } + ?.let { token -> authFacade.resolveAuthUser(token).awaitSingleOrNull() as? AuthUser } + + SystemActionLog( + uid = authUser?.uid, + ipAddress = event.ipAddress, + path = event.path, + httpMethod = event.method, + userAgent = event.userAgent, + host = event.host, + referer = event.referer, + extra = event.extra + ).run { systemActionLogService.record(this) } } } diff --git a/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt b/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt index 903443e8..cc0c920c 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt @@ -1,5 +1,6 @@ package com.oksusu.susu.api.event.model +import com.oksusu.susu.api.auth.model.AUTH_TOKEN_KEY import com.oksusu.susu.api.auth.model.AuthUser import com.oksusu.susu.api.extension.remoteIp import com.oksusu.susu.cache.model.OidcPublicKeysCacheModel @@ -49,6 +50,7 @@ data class SystemActionLogEvent( val userAgent: String?, val host: String?, val referer: String?, + val token: String?, val extra: String?, ) : BaseEvent() { companion object { @@ -66,6 +68,7 @@ data class SystemActionLogEvent( userAgent = request.headers[USER_AGENT].toString(), host = request.headers[HOST].toString(), referer = request.headers[REFERER].toString(), + token = request.headers[AUTH_TOKEN_KEY].toString(), extra = mapper.writeValueAsString(request.headers) ) } diff --git a/api/src/test/kotlin/com/oksusu/susu/api/post/application/BoardServiceTest.kt b/api/src/test/kotlin/com/oksusu/susu/api/post/application/BoardServiceTest.kt index 04f86ab3..1fd76972 100644 --- a/api/src/test/kotlin/com/oksusu/susu/api/post/application/BoardServiceTest.kt +++ b/api/src/test/kotlin/com/oksusu/susu/api/post/application/BoardServiceTest.kt @@ -11,6 +11,7 @@ import io.kotest.matchers.equals.shouldBeEqual import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.delay class BoardServiceTest : DescribeSpec({ val logger = KotlinLogging.logger { } @@ -25,6 +26,8 @@ class BoardServiceTest : DescribeSpec({ beforeSpec { boardService.refreshBoards() + + delay(100) } describe("scheduler") { diff --git a/batch/src/main/kotlin/com/oksusu/susu/batch/discord/job/ResendFailedSentDiscordMessageJob.kt b/batch/src/main/kotlin/com/oksusu/susu/batch/discord/job/ResendFailedSentDiscordMessageJob.kt new file mode 100644 index 00000000..d16a7613 --- /dev/null +++ b/batch/src/main/kotlin/com/oksusu/susu/batch/discord/job/ResendFailedSentDiscordMessageJob.kt @@ -0,0 +1,111 @@ +package com.oksusu.susu.batch.discord.job + +import com.oksusu.susu.cache.key.Cache +import com.oksusu.susu.cache.model.FailedSentDiscordMessageCache +import com.oksusu.susu.cache.service.CacheService +import com.oksusu.susu.client.discord.DiscordClient +import com.oksusu.susu.client.discord.model.DiscordMessageModel +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.* +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class ResendFailedSentDiscordMessageJob( + private val cacheService: CacheService, + private val discordClient: DiscordClient, +) { + private val logger = KotlinLogging.logger { } + + companion object { + private const val RESEND_BEFORE_MINUTES = 1L + } + + suspend fun resendFailedSentDiscordMessage() { + logger.info { "start resend failed sent discord message" } + + // 1분 전에 실패한 것이 타겟 (현재가 24분이면 23분을 말하는 것) + val targetTime = LocalDateTime.now().minusMinutes(RESEND_BEFORE_MINUTES) + + // 실패 메세지 조회 및 삭제 + val failedMessages = withContext(Dispatchers.IO) { + cacheService.sGetMembers(Cache.getFailedSentDiscordMessageCache(targetTime)) + } + + withContext(Dispatchers.IO) { + cacheService.sDelete(Cache.getFailedSentDiscordMessageCache(targetTime)) + } + + // 다수 메세지 token 별로 하나의 메세지로 병합 + val message = mergeFailedMessage(failedMessages) + + // 재전송 + runCatching { + coroutineScope { + val sendDeferreds = message.map { (token, message) -> + val discordMessageModel = DiscordMessageModel(content = message) + + async(Dispatchers.IO) { + discordClient.sendMessage( + message = discordMessageModel, + token = token, + withRecover = false + ) + } + }.toTypedArray() + + awaitAll(*sendDeferreds) + } + }.onFailure { + // 재전송 실패시 1분 뒤에 다시 보낼 수 있게, 1분 뒤에 보내는 메세지 목록에 추가 + logger.warn { "postpone resend discord message" } + + postponeResendTimeOfFailedMessage(targetTime, message) + } + + logger.info { "finish resend failed sent discord message" } + } + + private suspend fun mergeFailedMessage(failedMessages: List): Map { + val message = mutableMapOf() + + failedMessages.forEach { model -> + val recoverMsg = if (model.isStacked) { + model.message + } else { + "[RECOVER - ${model.failedAt} discord failure] ${model.message}" + } + + val stackedMessage = message[model.token] + + message[model.token] = if (stackedMessage == null) { + recoverMsg + } else { + "$stackedMessage\n$recoverMsg" + } + } + + return message + } + + private suspend fun postponeResendTimeOfFailedMessage(targetTime: LocalDateTime, message: Map) { + val nextTime = targetTime.plusMinutes(RESEND_BEFORE_MINUTES) + + coroutineScope { + message.map { (token, message) -> + val model = FailedSentDiscordMessageCache( + token = token, + message = message, + isStacked = true + ) + + async(Dispatchers.IO) { + cacheService.sSet( + cache = Cache.getFailedSentDiscordMessageCache(nextTime), + value = model + ) + } + } + } + } +} diff --git a/batch/src/main/kotlin/com/oksusu/susu/batch/discord/scheduler/ResendFailedSentDiscordMessageScheduler.kt b/batch/src/main/kotlin/com/oksusu/susu/batch/discord/scheduler/ResendFailedSentDiscordMessageScheduler.kt new file mode 100644 index 00000000..32d3485d --- /dev/null +++ b/batch/src/main/kotlin/com/oksusu/susu/batch/discord/scheduler/ResendFailedSentDiscordMessageScheduler.kt @@ -0,0 +1,39 @@ +package com.oksusu.susu.batch.discord.scheduler + +import com.oksusu.susu.batch.discord.job.ResendFailedSentDiscordMessageJob +import com.oksusu.susu.batch.slack.job.ResendFailedSentSlackMessageJob +import com.oksusu.susu.client.common.coroutine.ErrorPublishingCoroutineExceptionHandler +import com.oksusu.susu.common.extension.isProd +import com.oksusu.susu.common.extension.resolveCancellation +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.springframework.core.env.Environment +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class ResendFailedSentDiscordMessageScheduler( + private val environment: Environment, + private val resendFailedSentDiscordMessageJob: ResendFailedSentDiscordMessageJob, + private val coroutineExceptionHandler: ErrorPublishingCoroutineExceptionHandler, +) { + private val logger = KotlinLogging.logger { } + + @Scheduled( + fixedRate = 1000 * 60, + initialDelayString = "\${oksusu.scheduled-tasks.resend-failed-sent-discord-message.initial-delay:100}" + ) + fun resendFailedSentDiscordMessageJob() { + if (environment.isProd()) { + CoroutineScope(Dispatchers.IO + coroutineExceptionHandler.handler).launch { + runCatching { + resendFailedSentDiscordMessageJob.resendFailedSentDiscordMessage() + }.onFailure { e -> + logger.resolveCancellation("[BATCH] fail to run resendFailedSentDiscordMessageJob", e) + } + } + } + } +} diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/log/domain/SystemActionLog.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/log/domain/SystemActionLog.kt index 51de59fa..1d0713f5 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/log/domain/SystemActionLog.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/log/domain/SystemActionLog.kt @@ -1,12 +1,7 @@ package com.oksusu.susu.domain.log.domain import com.oksusu.susu.domain.common.BaseEntity -import jakarta.persistence.Column -import jakarta.persistence.Entity -import jakarta.persistence.GeneratedValue -import jakarta.persistence.GenerationType -import jakarta.persistence.Id -import jakarta.persistence.Table +import jakarta.persistence.* @Entity @Table(name = "system_action_log") @@ -15,6 +10,9 @@ class SystemActionLog( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = -1L, + @Column(name = "uid") + val uid: Long? = null, + @Column(name = "ip_address") val ipAddress: String? = null, diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryQRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryQRepository.kt index 7b5dfe8c..926e4ff0 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryQRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryQRepository.kt @@ -12,6 +12,7 @@ import java.time.LocalDateTime @Transactional(readOnly = true) interface ReportHistoryQRepository { + @Transactional fun updateAllCreatedAt(createdAt: LocalDateTime): Long } diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultQRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultQRepository.kt index 4ec6a409..802829ba 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultQRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultQRepository.kt @@ -12,6 +12,7 @@ import java.time.LocalDateTime @Transactional(readOnly = true) interface ReportResultQRepository { + @Transactional fun updateAllCreatedAt(createdAt: LocalDateTime): Long } diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryQRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryQRepository.kt index 57ae1aec..60428d38 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryQRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryQRepository.kt @@ -17,6 +17,7 @@ interface UserStatusHistoryQRepository { fun getUidByToStatusId(toStatusId: Long): List + @Transactional fun updateAllCreatedAt(createdAt: LocalDateTime): Long } diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusQRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusQRepository.kt index bb098301..bae6321d 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusQRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusQRepository.kt @@ -12,6 +12,7 @@ import java.time.LocalDateTime @Transactional(readOnly = true) interface UserStatusQRepository { + @Transactional fun updateAllCreatedAt(createdAt: LocalDateTime): Long } diff --git a/gradle.properties b/gradle.properties index 234be870..04db9e83 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.10 +version=1.1.11 kotlin.code.style=official diff --git a/sql/ddl/log/DDL.sql b/sql/ddl/log/DDL.sql index 5a9007fd..85a52c68 100644 --- a/sql/ddl/log/DDL.sql +++ b/sql/ddl/log/DDL.sql @@ -2,6 +2,7 @@ CREATE TABLE `system_action_log` ( `id` bigint NOT NULL AUTO_INCREMENT, + `uid` int DEFAULT NULL COMMENT 'uid', `host` varchar(255) DEFAULT NULL, `http_method` varchar(255) DEFAULT NULL, `ip_address` varchar(255) DEFAULT NULL, @@ -14,3 +15,4 @@ CREATE TABLE `system_action_log` PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT 'system log'; CREATE INDEX idx__created_at ON system_action_log (created_at); +CREATE INDEX idx__uid ON system_action_log (uid);