Skip to content

Commit ff8f3aa

Browse files
committed
add rate limiting functionality with user feedback to CommandExecutor
1 parent 0a25106 commit ff8f3aa

2 files changed

Lines changed: 38 additions & 8 deletions

File tree

src/main/kotlin/com/helltar/signai/Strings.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ object Strings {
55
const val CONTEXT_HAS_BEEN_REMOVED = "Context has been removed \uD83D\uDC4C"
66
const val MANY_REQUEST = "Please wait, I am processing your previous request \uD83E\uDD16"
77
const val CHAT_CONTEXT_EMPTY = "\uFE0F Empty"
8+
const val SLOWMODE = "Request limit reached. Try again in %d sec \uD83D\uDE0A"
89
}

src/main/kotlin/com/helltar/signai/bot/CommandExecutor.kt

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,60 @@ import io.github.oshai.kotlinlogging.KotlinLogging
66
import kotlinx.coroutines.CoroutineScope
77
import kotlinx.coroutines.Job
88
import kotlinx.coroutines.launch
9+
import kotlin.math.ceil
910

1011
class CommandExecutor(private val scope: CoroutineScope) {
1112

1213
private val requestsMap = hashMapOf<String, Job>()
14+
private val requestTimestamps = hashMapOf<String, MutableList<Long>>()
1315

1416
private companion object {
17+
const val LIMIT_COUNT = 5
18+
const val LIMIT_WINDOW = 3600_000L
1519
val log = KotlinLogging.logger {}
1620
}
1721

22+
private sealed interface LaunchResult {
23+
data object Success : LaunchResult
24+
data object Busy : LaunchResult
25+
data class RateLimited(val waitSeconds: Long) : LaunchResult
26+
}
27+
1828
fun execute(botCommand: BotCommand) {
1929
val key = botCommand.envelope.source
2030

21-
if (!launch(key) { botCommand.run() })
22-
scope.launch { botCommand.replyToMessage(Strings.MANY_REQUEST) }
31+
when (val result = tryLaunch(key) { botCommand.run() }) {
32+
is LaunchResult.Success -> {}
33+
is LaunchResult.Busy -> scope.launch { botCommand.replyToMessage(Strings.MANY_REQUEST) }
34+
is LaunchResult.RateLimited -> scope.launch { botCommand.replyToMessage(Strings.SLOWMODE.format(result.waitSeconds)) }
35+
}
2336
}
2437

25-
private fun launch(key: String, block: suspend () -> Unit): Boolean {
26-
if (requestsMap.containsKey(key))
27-
if (requestsMap[key]?.isCompleted == false)
28-
return false
38+
private fun tryLaunch(key: String, block: suspend () -> Unit): LaunchResult {
39+
if (requestsMap[key]?.isActive == true)
40+
return LaunchResult.Busy
41+
42+
val timestamps = requestTimestamps.getOrPut(key) { mutableListOf() }
43+
val currentTime = System.currentTimeMillis()
44+
val windowStart = currentTime - LIMIT_WINDOW
45+
46+
timestamps.removeAll { it < windowStart }
47+
48+
if (timestamps.size >= LIMIT_COUNT) {
49+
val oldestRequestTime = timestamps.first()
50+
val unblockTime = oldestRequestTime + LIMIT_WINDOW
51+
val waitMs = unblockTime - currentTime
52+
val waitSeconds = if (waitMs > 0) ceil(waitMs / 1000.0).toLong() else 1L
53+
log.debug { "rate limit for $key. wait: ${waitSeconds}s" }
54+
return LaunchResult.RateLimited(waitSeconds)
55+
}
56+
57+
timestamps.add(currentTime)
2958

30-
log.debug { "launch --> $key" }
59+
log.debug { "launch --> $key (requests: ${timestamps.size} / $LIMIT_COUNT)" }
3160

3261
requestsMap[key] = scope.launch { block() }
3362

34-
return true
63+
return LaunchResult.Success
3564
}
3665
}

0 commit comments

Comments
 (0)