diff --git a/CHANGELOG.md b/CHANGELOG.md index 150be7b043..1d217da284 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # TelegramBotAPI changelog +## 0.30.3 + +* `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 + * Now `ExceptionsOnlyLimiter` (previously `EmptyLimiter`) is a class + * `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 * `Common`: diff --git a/gradle.properties b/gradle.properties index ff5eda7a35..b78605ad23 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,12 +12,12 @@ klock_version=1.12.1 uuid_version=0.2.2 ktor_version=1.4.2 -micro_utils_version=0.3.0 +micro_utils_version=0.3.1 javax_activation_version=1.1.1 library_group=dev.inmo -library_version=0.30.2 +library_version=0.30.3 gradle_bintray_plugin_version=1.8.5 github_release_plugin_version=2.2.12 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..4c5ffe6922 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..11a25ddfac 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 @@ -1,5 +1,6 @@ package dev.inmo.tgbotapi.bot.Ktor.base +import dev.inmo.micro_utils.coroutines.safely import dev.inmo.tgbotapi.bot.Ktor.KtorCallFactory import dev.inmo.tgbotapi.bot.exceptions.newRequestException import dev.inmo.tgbotapi.requests.GetUpdates @@ -51,23 +52,17 @@ abstract class AbstractRequestCallFactory : KtorCallFactory { val content = response.receive() val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) - 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, - content, - "Can't get result object from $content" - ) - }) + return safely { + (responseObject.result?.let { + jsonFormatter.decodeFromJsonElement(request.resultDeserializer, it) + } ?: response.let { + throw newRequestException( + responseObject, + content, + "Can't get result object from $content" + ) + }) + } } } 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..477b4a48f8 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt @@ -0,0 +1,65 @@ +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.* +import io.ktor.client.features.ClientRequestException +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* + +/** + * This limiter will limit requests only after getting a [RetryAfterError] or [ClientRequestException] with + * [HttpStatusCode.TooManyRequests] status code. 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 + * + * @param defaultTooManyRequestsDelay This parameter will be used in case of getting [ClientRequestException] with + * [HttpStatusCode.TooManyRequests] as a parameter for delay like it would be [TooMuchRequestsException]. The reason of + * it is that in [ClientRequestException] there is no information about required delay between requests + */ +class ExceptionsOnlyLimiter( + private val defaultTooManyRequestsDelay: MilliSeconds = 1000L +) : RequestLimiter { + private val lockState = MutableStateFlow(false) + private suspend fun lock(timeMillis: MilliSeconds) { + try { + safely { + lockState.emit(true) + delay(timeMillis) + } + } finally { + lockState.emit(false) + } + } + + override suspend fun limit(block: suspend () -> T): T { + while (true) { + lockState.first { !it } + val result = safely({ + when (it) { + is TooMuchRequestsException -> { + lock(it.retryAfter.leftToRetry) + Result.failure(it) + } + is ClientRequestException -> { + if (it.response.status == HttpStatusCode.TooManyRequests) { + lock(defaultTooManyRequestsDelay) + } else { + throw it + } + 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/Common.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt index af9fb30db5..1ededf321e 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt @@ -28,6 +28,7 @@ typealias GooglePlaceId = String typealias GooglePlaceType = String typealias Seconds = Int +typealias MilliSeconds = Long typealias LongSeconds = Long typealias Meters = Float 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