feat: Switch from coroutines to virtual threads

This commit is contained in:
2025-07-27 00:38:53 +03:00
parent 7e3a4ddb3a
commit cd62c5652d
5 changed files with 32 additions and 38 deletions

View File

@@ -3,7 +3,6 @@ plugins {
kotlin("plugin.spring") version "2.2.0" kotlin("plugin.spring") version "2.2.0"
id("org.springframework.boot") version "3.4.4" id("org.springframework.boot") version "3.4.4"
id("io.spring.dependency-management") version "1.1.7" id("io.spring.dependency-management") version "1.1.7"
id("com.google.cloud.tools.jib") version "3.4.5"
id("com.diffplug.spotless") version "7.0.3" id("com.diffplug.spotless") version "7.0.3"
} }
@@ -55,10 +54,3 @@ spotless {
ktfmt("0.54").kotlinlangStyle() ktfmt("0.54").kotlinlangStyle()
} }
} }
jib {
container {
// disable spring devtools
jvmFlags = listOf("-Dspring.devtools.restart.enabled=false")
}
}

View File

@@ -13,6 +13,8 @@ import com.pischule.memevizor.bot.handler.ThisCommandHandlerService
import com.pischule.memevizor.util.getMedia import com.pischule.memevizor.util.getMedia
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.oshai.kotlinlogging.withLoggingContext import io.github.oshai.kotlinlogging.withLoggingContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
@@ -35,13 +37,17 @@ class BotConfiguration(
} }
} }
@Bean fun botPollingExecutor(): ExecutorService = Executors.newSingleThreadExecutor()
@Bean fun botHandlerExecutor(): ExecutorService = Executors.newVirtualThreadPerTaskExecutor()
private fun Bot.Builder.setupDispatchers() = dispatch { private fun Bot.Builder.setupDispatchers() = dispatch {
message { withLogAndErrorHandling(message) { thisCommandHandlerService.create(this) } } message { handleMessage(message) { thisCommandHandlerService.create(this) } }
photos { handleMedia() } photos { handleMedia() }
video { handleMedia() } video { handleMedia() }
videoNote { handleMedia() } videoNote { handleMedia() }
command("whoami") { command("whoami") {
withLogAndErrorHandling(message) { handleMessage(message) {
bot.sendMessage( bot.sendMessage(
chatId = ChatId.fromId(message.chat.id), chatId = ChatId.fromId(message.chat.id),
text = "chatId: `${message.chat.id}`\nuserId: `${message.from?.id}`", text = "chatId: `${message.chat.id}`\nuserId: `${message.from?.id}`",
@@ -52,11 +58,12 @@ class BotConfiguration(
} }
} }
private suspend fun MediaHandlerEnvironment<*>.handleMedia() { private fun MediaHandlerEnvironment<*>.handleMedia() {
withLogAndErrorHandling(message) { mediaHandlerService.create(this) } handleMessage(message) { mediaHandlerService.create(this) }
} }
private suspend fun withLogAndErrorHandling(message: Message, block: suspend () -> Unit) { private fun handleMessage(message: Message, block: () -> Unit) {
botHandlerExecutor().execute {
withLoggingContext( withLoggingContext(
"message_id" to message.messageId.toString(), "message_id" to message.messageId.toString(),
"chat_id" to message.chat.id.toString(), "chat_id" to message.chat.id.toString(),
@@ -72,3 +79,4 @@ class BotConfiguration(
} }
} }
} }
}

View File

@@ -4,29 +4,23 @@ import com.github.kotlintelegrambot.Bot
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy import jakarta.annotation.PreDestroy
import java.util.concurrent.ExecutorService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
@Service @Service
class BotService(private val bot: Bot) { class BotService(private val bot: Bot, private val botPollingExecutor: ExecutorService) {
private var pollingThread: Thread? = null
@PostConstruct @PostConstruct
fun start() { fun start() {
pollingThread = botPollingExecutor.submit(bot::startPolling)
Thread { bot.startPolling() }
.apply {
name = "telegram-bot-polling"
start()
}
logger.info { "Bot service started" } logger.info { "Bot service started" }
} }
@PreDestroy @PreDestroy
fun stop() { fun stop() {
bot.stopPolling() bot.stopPolling()
pollingThread?.join(1000)
logger.info { "Bot service stopped" } logger.info { "Bot service stopped" }
} }
} }

View File

@@ -12,7 +12,7 @@ private val logger = KotlinLogging.logger {}
@Service @Service
class MediaHandlerService(private val botProps: BotProps) { class MediaHandlerService(private val botProps: BotProps) {
suspend fun create(env: MediaHandlerEnvironment<*>) { fun create(env: MediaHandlerEnvironment<*>) {
if (shouldForwardMessage(env)) { if (shouldForwardMessage(env)) {
forwardMessage(env) forwardMessage(env)
} }
@@ -24,7 +24,7 @@ class MediaHandlerService(private val botProps: BotProps) {
return env.message.chat.id != botProps.forwardChatId return env.message.chat.id != botProps.forwardChatId
} }
private suspend fun forwardMessage(env: MediaHandlerEnvironment<*>) { private fun forwardMessage(env: MediaHandlerEnvironment<*>) {
env.bot env.bot
.forwardMessage( .forwardMessage(
chatId = ChatId.fromId(botProps.forwardChatId), chatId = ChatId.fromId(botProps.forwardChatId),
@@ -37,7 +37,7 @@ class MediaHandlerService(private val botProps: BotProps) {
) )
} }
private suspend fun reactToMessage(env: MediaHandlerEnvironment<*>, emoji: String) { private fun reactToMessage(env: MediaHandlerEnvironment<*>, emoji: String) {
env.bot env.bot
.setMessageReaction( .setMessageReaction(
chatId = ChatId.fromId(env.message.chat.id), chatId = ChatId.fromId(env.message.chat.id),

View File

@@ -23,7 +23,7 @@ class ThisCommandHandlerService(
private val confirmCommands = listOf("this", "!soxok") private val confirmCommands = listOf("this", "!soxok")
private val mediaFileName = "media" private val mediaFileName = "media"
suspend fun create(env: MessageHandlerEnvironment) { fun create(env: MessageHandlerEnvironment) {
if (!shouldHandleMessage(env)) return if (!shouldHandleMessage(env)) return
val replyToMessage = env.message.replyToMessage ?: return val replyToMessage = env.message.replyToMessage ?: return
@@ -54,7 +54,7 @@ class ThisCommandHandlerService(
return isApprover && isConfirmCommand return isApprover && isConfirmCommand
} }
private suspend fun reactToMessage(env: MessageHandlerEnvironment, emoji: String) { private fun reactToMessage(env: MessageHandlerEnvironment, emoji: String) {
env.bot env.bot
.setMessageReaction( .setMessageReaction(
chatId = ChatId.fromId(env.message.chat.id), chatId = ChatId.fromId(env.message.chat.id),