@@ -6,31 +6,60 @@ import io.github.oshai.kotlinlogging.KotlinLogging
66import kotlinx.coroutines.CoroutineScope
77import kotlinx.coroutines.Job
88import kotlinx.coroutines.launch
9+ import kotlin.math.ceil
910
1011class 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