From 8e64205f536f67e5dd2e31fa3b284d2aee89b3d5 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 26 Dec 2022 20:49:29 +0600 Subject: [PATCH 1/9] several improvements in requests limiters --- CHANGELOG.md | 2 ++ gradle.properties | 2 +- .../tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt | 4 +++- .../inmo/tgbotapi/bot/settings/limiters/NoLimitsLimiter.kt | 5 +++++ 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/NoLimitsLimiter.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7dcd3b05..3f777d1d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # TelegramBotAPI changelog +## 4.2.3 + ## 4.2.2 * `Versions`: diff --git a/gradle.properties b/gradle.properties index d65eeb7079..00e0ade84e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ kotlin.incremental=true kotlin.incremental.js=true library_group=dev.inmo -library_version=4.2.2 +library_version=4.2.3 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 index 723e88fd2e..7d908afbb1 100644 --- 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 @@ -36,7 +36,9 @@ class ExceptionsOnlyLimiter( override suspend fun limit(block: suspend () -> T): T { while (true) { - lockState.first { !it } + if (lockState.value) { + lockState.first { it == false } + } var throwable: Throwable? = null val result = safely({ throwable = when (it) { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/NoLimitsLimiter.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/NoLimitsLimiter.kt new file mode 100644 index 0000000000..09dfda571b --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/NoLimitsLimiter.kt @@ -0,0 +1,5 @@ +package dev.inmo.tgbotapi.bot.settings.limiters + +object NoLimitsLimiter : RequestLimiter { + override suspend fun limit(block: suspend () -> T): T = block() +} From 91307f3ebf0ae8419b2cf4fbc4ba4e158e87a5fc Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 26 Dec 2022 21:37:48 +0600 Subject: [PATCH 2/9] improvements in ExceptionsOnlyLimiter --- .../tgbotapi/bot/ktor/KtorRequestsExecutor.kt | 2 +- .../limiters/ExceptionsOnlyLimiter.kt | 64 ++++++++++++++----- .../bot/settings/limiters/RequestLimiter.kt | 4 ++ 3 files changed, 53 insertions(+), 17 deletions(-) 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 c9a3663fe5..b6b3cd79de 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 @@ -51,7 +51,7 @@ class KtorRequestsExecutor( override suspend fun execute(request: Request): T { return runCatchingSafely { pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) - requestsLimiter.limit { + requestsLimiter.limit(request) { var result: T? = null lateinit var factoryHandledRequest: KtorCallFactory for (potentialFactory in callsFactories) { 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 index 7d908afbb1..0ad5e2a14b 100644 --- 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 @@ -2,6 +2,7 @@ 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.requests.abstracts.Request import dev.inmo.tgbotapi.types.MilliSeconds import dev.inmo.tgbotapi.types.RetryAfterError import io.ktor.client.plugins.ClientRequestException @@ -9,46 +10,77 @@ import io.ktor.http.HttpStatusCode import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock /** * 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 + * [HttpStatusCode.TooManyRequests] status code. When block throws [TooMuchRequestsException] or [RetryAfterError], + * in the limiter will be created special [Mutex] for the key, defined in [requestKeyFactory], and this mutex will be + * locked for some time based on type of error. See [limit] for more info * * @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 + * @param requestKeyFactory This parameter define how to determine request key in limiter */ class ExceptionsOnlyLimiter( - private val defaultTooManyRequestsDelay: MilliSeconds = 1000L + private val defaultTooManyRequestsDelay: MilliSeconds = 1000L, + private val requestKeyFactory: suspend (Request<*>) -> Any = { it::class } ) : RequestLimiter { - private val lockState = MutableStateFlow(false) - private suspend fun lock(timeMillis: MilliSeconds) { - try { + /** + * Should be used for all [mutexesMap] changes + */ + private val lockMutex = Mutex() + + /** + * Contains [Mutex]es for [Any] keys. If [Mutex] is presented it means that [lock] function has been called and + * that mutex should be locked for some time + */ + private val mutexesMap = mutableMapOf() + private suspend fun lock( + key: Any, + timeMillis: MilliSeconds + ) { + val mutex = Mutex() + mutex.withLock { safely { - lockState.emit(true) + lockMutex.withLock { + mutexesMap[key] = mutex + } delay(timeMillis) + lockMutex.withLock { + mutexesMap.remove(key) + } } - } finally { - lockState.emit(false) } } - override suspend fun limit(block: suspend () -> T): T { + /** + * Just call [block] + */ + override suspend fun limit(block: suspend () -> T): T = block() + + /** + * Will take a key for [request] using [requestKeyFactory] and try to retrieve [Mutex] by that key. In case if [Mutex] + * presented it will wait while [Mutex] is locked. After that operations completed, method will call + * [limit] with [block] inside of [safely] and in case of exception will call internal [lock] method + */ + override suspend fun limit(request: Request, block: suspend () -> T): T { + val key = requestKeyFactory(request) while (true) { - if (lockState.value) { - lockState.first { it == false } - } + // do nothing, just wait for unlock in case when mutex is presented in mutexesMap + lockMutex.withLock { mutexesMap[key] } ?.takeIf { it.isLocked } ?.withLock { } var throwable: Throwable? = null val result = safely({ throwable = when (it) { is TooMuchRequestsException -> { - lock(it.retryAfter.leftToRetry) + lock(key, it.retryAfter.leftToRetry) it } is ClientRequestException -> { if (it.response.status == HttpStatusCode.TooManyRequests) { - lock(defaultTooManyRequestsDelay) + lock(key, defaultTooManyRequestsDelay) } else { throw it } @@ -58,7 +90,7 @@ class ExceptionsOnlyLimiter( } null }) { - block() + limit(block) } if (throwable == null) { return result!! diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/RequestLimiter.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/RequestLimiter.kt index a23ed0b97c..dd08878577 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/RequestLimiter.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/RequestLimiter.kt @@ -1,8 +1,12 @@ package dev.inmo.tgbotapi.bot.settings.limiters +import dev.inmo.tgbotapi.requests.abstracts.Request + interface RequestLimiter { /** * Use limit for working of block (like delay between or after, for example) */ suspend fun limit(block: suspend () -> T): T + + suspend fun limit(request: Request, block: suspend () -> T) = limit(block) } From f686be0271f2eb8d389ae7763db9b58de1012690 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 27 Dec 2022 22:22:03 +0600 Subject: [PATCH 3/9] Update ExceptionsOnlyLimiter.kt --- .../limiters/ExceptionsOnlyLimiter.kt | 43 +++---------------- 1 file changed, 6 insertions(+), 37 deletions(-) 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 index 0ad5e2a14b..4547cb0f53 100644 --- 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 @@ -26,7 +26,6 @@ import kotlinx.coroutines.sync.withLock */ class ExceptionsOnlyLimiter( private val defaultTooManyRequestsDelay: MilliSeconds = 1000L, - private val requestKeyFactory: suspend (Request<*>) -> Any = { it::class } ) : RequestLimiter { /** * Should be used for all [mutexesMap] changes @@ -59,42 +58,12 @@ class ExceptionsOnlyLimiter( /** * Just call [block] */ - override suspend fun limit(block: suspend () -> T): T = block() - - /** - * Will take a key for [request] using [requestKeyFactory] and try to retrieve [Mutex] by that key. In case if [Mutex] - * presented it will wait while [Mutex] is locked. After that operations completed, method will call - * [limit] with [block] inside of [safely] and in case of exception will call internal [lock] method - */ - override suspend fun limit(request: Request, block: suspend () -> T): T { - val key = requestKeyFactory(request) - while (true) { - // do nothing, just wait for unlock in case when mutex is presented in mutexesMap - lockMutex.withLock { mutexesMap[key] } ?.takeIf { it.isLocked } ?.withLock { } - var throwable: Throwable? = null - val result = safely({ - throwable = when (it) { - is TooMuchRequestsException -> { - lock(key, it.retryAfter.leftToRetry) - it - } - is ClientRequestException -> { - if (it.response.status == HttpStatusCode.TooManyRequests) { - lock(key, defaultTooManyRequestsDelay) - } else { - throw it - } - it - } - else -> throw it - } - null - }) { - limit(block) - } - if (throwable == null) { - return result!! - } + override suspend fun limit(block: suspend () -> T): T { + try { + block() + } catch (e: TooMuchRequestsException) { + delay(e.retryAfter.leftToRetry) + limit(block) } } } From f9a9f958ba689e28df23461ed1c8f72df5782bf2 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 27 Dec 2022 22:25:26 +0600 Subject: [PATCH 4/9] Update ExceptionsOnlyLimiter.kt --- .../limiters/ExceptionsOnlyLimiter.kt | 28 ------------------- 1 file changed, 28 deletions(-) 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 index 4547cb0f53..3b698986a2 100644 --- 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 @@ -27,34 +27,6 @@ import kotlinx.coroutines.sync.withLock class ExceptionsOnlyLimiter( private val defaultTooManyRequestsDelay: MilliSeconds = 1000L, ) : RequestLimiter { - /** - * Should be used for all [mutexesMap] changes - */ - private val lockMutex = Mutex() - - /** - * Contains [Mutex]es for [Any] keys. If [Mutex] is presented it means that [lock] function has been called and - * that mutex should be locked for some time - */ - private val mutexesMap = mutableMapOf() - private suspend fun lock( - key: Any, - timeMillis: MilliSeconds - ) { - val mutex = Mutex() - mutex.withLock { - safely { - lockMutex.withLock { - mutexesMap[key] = mutex - } - delay(timeMillis) - lockMutex.withLock { - mutexesMap.remove(key) - } - } - } - } - /** * Just call [block] */ From 33a1701f5be943a30f62171d8d9b760670c5d56d Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 27 Dec 2022 22:30:47 +0600 Subject: [PATCH 5/9] Update ExceptionsOnlyLimiter.kt --- .../limiters/ExceptionsOnlyLimiter.kt | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) 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 index 3b698986a2..a8cdfbc7f0 100644 --- 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 @@ -14,24 +14,11 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock /** - * This limiter will limit requests only after getting a [RetryAfterError] or [ClientRequestException] with - * [HttpStatusCode.TooManyRequests] status code. When block throws [TooMuchRequestsException] or [RetryAfterError], - * in the limiter will be created special [Mutex] for the key, defined in [requestKeyFactory], and this mutex will be - * locked for some time based on type of error. See [limit] for more info - * - * @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 - * @param requestKeyFactory This parameter define how to determine request key in limiter + * Simple limiter which will lock any request when TooMuchRequestsExceptions is thrown and rerun request after lock time */ -class ExceptionsOnlyLimiter( - private val defaultTooManyRequestsDelay: MilliSeconds = 1000L, -) : RequestLimiter { - /** - * Just call [block] - */ +object ExceptionsOnlyLimiter : RequestLimiter { override suspend fun limit(block: suspend () -> T): T { - try { + return try { block() } catch (e: TooMuchRequestsException) { delay(e.retryAfter.leftToRetry) From 0fff553ce1d8209aeb53340b214d7865cbff7434 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Tue, 27 Dec 2022 22:31:42 +0600 Subject: [PATCH 6/9] Update KtorRequestsExecutor.kt --- .../kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b6b3cd79de..88990f0996 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 @@ -30,7 +30,7 @@ class KtorRequestsExecutor( client: HttpClient = HttpClient(), callsFactories: List = emptyList(), excludeDefaultFactories: Boolean = false, - private val requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter(), + private val requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter, private val jsonFormatter: Json = nonstrictJsonFormat, private val pipelineStepsHolder: KtorPipelineStepsHolder = KtorPipelineStepsHolder ) : BaseRequestsExecutor(telegramAPIUrlsKeeper) { @@ -111,7 +111,7 @@ class KtorRequestsExecutorBuilder( var client: HttpClient = HttpClient() var callsFactories: List = emptyList() var excludeDefaultFactories: Boolean = false - var requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter() + var requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter var jsonFormatter: Json = nonstrictJsonFormat fun build() = KtorRequestsExecutor(telegramAPIUrlsKeeper, client, callsFactories, excludeDefaultFactories, requestsLimiter, jsonFormatter) From 2a32654d575268f8792b677b99199242b52998fd Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 28 Dec 2022 09:05:32 +0600 Subject: [PATCH 7/9] update miroutils and fill changelog --- CHANGELOG.md | 5 +++++ gradle/libs.versions.toml | 2 +- .../bot/settings/limiters/ExceptionsOnlyLimiter.kt | 12 +----------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f777d1d9d..a472050742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## 4.2.3 +* `Versions`: + * `MicroUtils`: `0.16.2` -> `0.16.4` +* `Core`: + * Simplify default `RequestsLimiter` (`ExceptionsOnlyLimiter`) + ## 4.2.2 * `Versions`: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 98d24fa96a..2e7f91af54 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ ktor = "2.2.1" ksp = "1.7.22-1.0.8" kotlin-poet = "1.12.0" -microutils = "0.16.2" +microutils = "0.16.4" github-release-plugin = "2.4.1" dokka = "1.7.20" 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 index a8cdfbc7f0..41b7b002b6 100644 --- 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 @@ -1,20 +1,10 @@ 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.requests.abstracts.Request -import dev.inmo.tgbotapi.types.MilliSeconds -import dev.inmo.tgbotapi.types.RetryAfterError -import io.ktor.client.plugins.ClientRequestException -import io.ktor.http.HttpStatusCode import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock /** - * Simple limiter which will lock any request when TooMuchRequestsExceptions is thrown and rerun request after lock time + * Simple limiter which will lock any request when [TooMuchRequestsExceptions] is thrown and rerun request after lock time */ object ExceptionsOnlyLimiter : RequestLimiter { override suspend fun limit(block: suspend () -> T): T { From e09ea9a9b4251ace924f3465caecd739830afcea Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 28 Dec 2022 09:11:08 +0600 Subject: [PATCH 8/9] upfill changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a472050742..0c5f54dad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * `Versions`: * `MicroUtils`: `0.16.2` -> `0.16.4` * `Core`: - * Simplify default `RequestsLimiter` (`ExceptionsOnlyLimiter`) + * Simplify default `RequestsLimiter` (`ExceptionsOnlyLimiter`) (thanks to @y9san9 for help) ## 4.2.2 From 4197e13c5485b5cb63ba5f56199e2c27815c0dc5 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 28 Dec 2022 09:12:54 +0600 Subject: [PATCH 9/9] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c5f54dad4..048b8016ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * `Versions`: * `MicroUtils`: `0.16.2` -> `0.16.4` * `Core`: - * Simplify default `RequestsLimiter` (`ExceptionsOnlyLimiter`) (thanks to @y9san9 for help) + * Simplify default `RequestsLimiter` (`ExceptionsOnlyLimiter`) (thanks to [@y9san9](https://github.com/y9san9) for help) ## 4.2.2