diff --git a/CHANGELOG.md b/CHANGELOG.md index fc493ecbaa..ad9d969f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ * `Common`: * `Version`: * `MicroUtils`: `0.3.0` -> `0.3.1` +* `Core`: + * New type of requests exceptions `TooMuchRequestsException`. In fact it will be rare case when you will get this + exception + * `EmptyLimiter` has been renamed to `ExceptionsOnlyLimiter` and currently will stop requests after + `TooMuchRequestsException` happen until retry time is actual + * `AbstractRequestCallFactory` currently will not look at the response and wait if it have `RetryAfter` error. New + behaviour aimed on delegating of this work to `RequestsLimiter` ## 0.30.2 diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorRequestsExecutor.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorRequestsExecutor.kt index 949c55caf0..a3e99ba631 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorRequestsExecutor.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorRequestsExecutor.kt @@ -4,8 +4,7 @@ import dev.inmo.micro_utils.coroutines.safely import dev.inmo.tgbotapi.bot.BaseRequestsExecutor import dev.inmo.tgbotapi.bot.Ktor.base.* import dev.inmo.tgbotapi.bot.exceptions.newRequestException -import dev.inmo.tgbotapi.bot.settings.limiters.EmptyLimiter -import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter +import dev.inmo.tgbotapi.bot.settings.limiters.* import dev.inmo.tgbotapi.requests.abstracts.Request import dev.inmo.tgbotapi.types.Response import dev.inmo.tgbotapi.utils.* @@ -19,7 +18,7 @@ class KtorRequestsExecutor( client: HttpClient = HttpClient(), callsFactories: List = emptyList(), excludeDefaultFactories: Boolean = false, - private val requestsLimiter: RequestLimiter = EmptyLimiter, + private val requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter, private val jsonFormatter: Json = nonstrictJsonFormat ) : BaseRequestsExecutor(telegramAPIUrlsKeeper) { private val callsFactories: List = callsFactories.run { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/AbstractRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/AbstractRequestCallFactory.kt index a925ed38c2..1abf10a653 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/AbstractRequestCallFactory.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/AbstractRequestCallFactory.kt @@ -53,14 +53,6 @@ abstract class AbstractRequestCallFactory : KtorCallFactory { return (responseObject.result?.let { jsonFormatter.decodeFromJsonElement(request.resultDeserializer, it) - } ?: responseObject.parameters?.let { - val error = it.error - if (error is RetryAfterError) { - delay(error.leftToRetry) - makeCall(client, urlsKeeper, request, jsonFormatter) - } else { - null - } } ?: response.let { throw newRequestException( responseObject, diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/exceptions/RequestException.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/exceptions/RequestException.kt index b354610645..f792616fc2 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/exceptions/RequestException.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/exceptions/RequestException.kt @@ -1,6 +1,7 @@ package dev.inmo.tgbotapi.bot.exceptions -import dev.inmo.tgbotapi.types.Response +import com.soywiz.klock.DateTime +import dev.inmo.tgbotapi.types.* import io.ktor.utils.io.errors.IOException fun newRequestException( @@ -16,6 +17,13 @@ fun newRequestException( description == "Unauthorized" -> UnauthorizedException(response, plainAnswer, message, cause) description.contains("PHOTO_INVALID_DIMENSIONS") -> InvalidPhotoDimensionsException(response, plainAnswer, message, cause) description.contains("wrong file identifier") -> WrongFileIdentifierException(response, plainAnswer, message, cause) + description.contains("Too Many Requests") -> TooMuchRequestsException( + (response.parameters ?.error as? RetryAfterError) ?: RetryAfterError(60, DateTime.now().unixMillisLong), + response, + plainAnswer, + message, + cause + ) else -> null } } ?: CommonRequestException(response, plainAnswer, message, cause) @@ -49,3 +57,6 @@ class InvalidPhotoDimensionsException(response: Response, plainAnswer: String, m class WrongFileIdentifierException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) : RequestException(response, plainAnswer, message, cause) + +class TooMuchRequestsException(val retryAfter: RetryAfterError, response: Response, plainAnswer: String, message: String?, cause: Throwable?) : + RequestException(response, plainAnswer, message, cause) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/EmptyLimiter.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/EmptyLimiter.kt deleted file mode 100644 index d643aa1d9d..0000000000 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/EmptyLimiter.kt +++ /dev/null @@ -1,5 +0,0 @@ -package dev.inmo.tgbotapi.bot.settings.limiters - -object EmptyLimiter : RequestLimiter { - override suspend fun limit(block: suspend () -> T): T = block() -} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt new file mode 100644 index 0000000000..ca792b1575 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt @@ -0,0 +1,43 @@ +package dev.inmo.tgbotapi.bot.settings.limiters + +import dev.inmo.micro_utils.coroutines.safely +import dev.inmo.tgbotapi.bot.exceptions.TooMuchRequestsException +import dev.inmo.tgbotapi.types.RetryAfterError +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* + +/** + * This limiter will limit requests only after getting a [RetryAfterError] from incoming [block]s. Important thing is + * that in case if some of block has been blocked, all the others will wait until it will be possible to be called + */ +object ExceptionsOnlyLimiter : RequestLimiter { + private val lockState = MutableStateFlow(false) + override suspend fun limit(block: suspend () -> T): T { + while (true) { + lockState.first { !it } + val result = safely({ + if (it is TooMuchRequestsException) { + try { + safely { + lockState.emit(true) + delay(it.retryAfter.leftToRetry) + } + } finally { + lockState.emit(false) + } + Result.failure(it) + } else { + throw it + } + }) { + Result.success(block()) + } + if (result.isSuccess) { + return result.getOrNull()!! + } + } + } +} + +@Deprecated("Renamed", ReplaceWith("ExceptionsOnlyLimiter", "dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter")) +typealias EmptyLimiter = ExceptionsOnlyLimiter diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/RequestError.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/RequestError.kt index 8ede26681c..4a53843390 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/RequestError.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/RequestError.kt @@ -5,7 +5,7 @@ import com.soywiz.klock.DateTime sealed class RequestError data class RetryAfterError( - val seconds: Long, + val seconds: Seconds, val startCountingMillis: Long ) : RequestError() { val canContinue = (seconds * 1000L) + startCountingMillis diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ResponseParametersRaw.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ResponseParametersRaw.kt index c5f28ade7f..71c837a1e9 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ResponseParametersRaw.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/ResponseParametersRaw.kt @@ -8,7 +8,7 @@ data class ResponseParametersRaw( @SerialName("migrate_to_chat_id") private val migrateToChatId: ChatId? = null, @SerialName("retry_after") - private val retryAfter: Long? = null + private val retryAfter: Seconds? = null ) { @Transient private val createTime: Long = DateTime.now().unixMillisLong