如何让 OkHttpClient 遵守 REST API 的速率限制?

How can I get an OkHttpClient to comply with a REST API's rate limits?

我正在编写一个 Android 应用程序,它会频繁请求 REST API 服务。此服务的硬请求限制为每秒 2 个请求,之后它将 return HTTP 503 没有其他信息。我想成为一名优秀的开发人员并限制我的应用程序的速率以符合服务的要求(即,在我的请求成功之前不要重试向服务发送垃圾邮件)但事实证明这很难做到。

我正在尝试限制速率 OkHttpClient specifically, because I can cleanly slot an instance of a client into both Coil and Retrofit 以便我的所有网络请求都受到限制,而无需在调用站点为它们中的任何一个做任何额外的工作:我可以调用 enqueue()不假思索。然后重要的是我能够在 enqueue()ed 请求上调用 cancel()dispose(),这样我就可以避免在用户更改页面时进行不必要的网络请求,例如。

我开始关注 this question 的答案,该答案在 OkHttp Interceptor 中使用了 Guava RateLimiter,并且效果很好!直到我意识到我需要能够取消挂起的请求,而你不能用 Guava 的 RateLimiter 来做到这一点,因为它会在 acquire() 时阻塞当前线程,从而阻止请求立即取消。

然后我出于某种原因尝试关注 this suggestion, where you call Thread.interrupt() to get the blocked interceptor to resume, but it won't work because Guava RateLimiters block uninterruptibly。 (注意:做 tryAcquire() 而不是 acquire() 然后中断地 Thread.sleep()ing 不是一个很好的解决方案,因为你不知道要睡多久。)

然后我开始考虑放弃 Guava 解决方案并实现一个自定义的 ExecutorService ,它将请求保存在一个队列中,该队列将由计时器定期调度,但对于一些可能的事情来说似乎有很多复杂的工作或可能无法工作,我现在正在远离杂草。有没有更好或更简单的方法来做我想做的事?

最终我决定不将 OkHttpClient 配置为速率限制。对于我的具体用例,我 99% 的请求都是通过 Coil 完成的,剩下的很少见,是通过 Retrofit 完成的,所以我决定:

  • 根本不使用 Interceptor,而是允许通过客户端的任何请求照常进行。假设改造请求很少发生,我不关心限制它们。
  • 制作一个 class,其中包含一个 Queue 和一个定期弹出和运行任务的 Timer。它并不聪明,但效果出奇地好。我的 Coil 图像请求被放入队列中,这样当它们到达前面时就会调用 imageLoader.enqueue(),但如果我需要取消请求,它们也可以从队列中清除。
  • 如果我在这之后以某种方式错误地超过了速率限制(技术上可能,但不太可能),我可以接受 OkHttp 偶尔不得不重试请求而不是担心永远不会达到限制。

这是我想出的(非常简单的)队列:

import java.util.*

class RateLimitedQueue(private val millisecondsPerTask: Long) {
    private val timer = Timer()
    private val queue = ArrayDeque<Task>()

    init {
        timer.scheduleAtFixedRate(RunTaskTimerTask(this), 0, millisecondsPerTask)
    }

    private class RunTaskTimerTask(val parent: RateLimitedQueue) : TimerTask() {
        override fun run() {
            synchronized(parent.queue) {
                if (!parent.queue.isEmpty())
                    parent.queue.pop().run()
            }
        }
    }

    fun interface Task {
        fun run()
    }

    fun add(t: Task) {
        synchronized(queue) {
            queue.add(t)
        }
    }

    fun remove(t: Task) {
        synchronized(queue) {
            queue.remove(t)
        }
    }

    fun removeAll(filter: (Task) -> Boolean) {
        synchronized(queue) {
            queue.removeAll(filter)
        }
    }
}

我对这个解决方案很满意,但有一些值得注意的地方:

  • Timer+Queue 不是很聪明:它实际上只是每 X 毫秒检查一次任务,所以如果一个任务在很长一段时间之间到达一个空队列延迟,它可能会不必要地等待。你可以想出更优雅的东西,但我发现它对我的 500 毫秒延迟来说已经足够好了。
  • 它不如 Interceptor 干净,因为它不在 Retrofit 和 Coil 之间共享。
  • 技术上可以超过速率限制,但对于我来说,这是可以接受的。
  • 任务到达队列前端后,您无法取消任务;你只能在它们被执行之前从队列中移除它们。

创建一个队列来包装您的 comm 调用,该队列使用 timer/delayed 协程,一旦队列为空就会终止对下一个调用的检查(这样您就没有计时器 运行不必要地)。我在移动设备和桌面设备上都构建了这样的逻辑......足够简单到 IMPL 并完成工作。

如果你使用的是 Kotlin ,那么你可以使用带有 retorift 的协程。 在协程中有作业 ID ,用于取消您可以使用的任何请求 apiJob?.cancel()

对于干净的解释 https://proandroiddev.com/retrofit-cancelling-multiple-api-calls-4dc6b7dc0bbd