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"
id("org.springframework.boot") version "3.4.4"
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"
}
@@ -55,10 +54,3 @@ spotless {
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 io.github.oshai.kotlinlogging.KotlinLogging
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.context.annotation.Bean
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 {
message { withLogAndErrorHandling(message) { thisCommandHandlerService.create(this) } }
message { handleMessage(message) { thisCommandHandlerService.create(this) } }
photos { handleMedia() }
video { handleMedia() }
videoNote { handleMedia() }
command("whoami") {
withLogAndErrorHandling(message) {
handleMessage(message) {
bot.sendMessage(
chatId = ChatId.fromId(message.chat.id),
text = "chatId: `${message.chat.id}`\nuserId: `${message.from?.id}`",
@@ -52,22 +58,24 @@ class BotConfiguration(
}
}
private suspend fun MediaHandlerEnvironment<*>.handleMedia() {
withLogAndErrorHandling(message) { mediaHandlerService.create(this) }
private fun MediaHandlerEnvironment<*>.handleMedia() {
handleMessage(message) { mediaHandlerService.create(this) }
}
private suspend fun withLogAndErrorHandling(message: Message, block: suspend () -> Unit) {
withLoggingContext(
"message_id" to message.messageId.toString(),
"chat_id" to message.chat.id.toString(),
"from_user_id" to message.from?.id.toString(),
"from_user_username" to message.from?.username.toString(),
"file_id" to (message.getMedia()?.fileId),
) {
try {
block.invoke()
} catch (e: Exception) {
logger.error(e) { "Error while handling message" }
private fun handleMessage(message: Message, block: () -> Unit) {
botHandlerExecutor().execute {
withLoggingContext(
"message_id" to message.messageId.toString(),
"chat_id" to message.chat.id.toString(),
"from_user_id" to message.from?.id.toString(),
"from_user_username" to message.from?.username.toString(),
"file_id" to (message.getMedia()?.fileId),
) {
try {
block.invoke()
} catch (e: Exception) {
logger.error(e) { "Error while handling message" }
}
}
}
}

View File

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

View File

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

View File

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