【问题标题】:How can I get an OkHttpClient to comply with a REST API's rate limits?如何让 OkHttpClient 遵守 REST API 速率限制?
【发布时间】:2021-06-15 10:52:33
【问题描述】:

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

我正在尝试对OkHttpClient 进行速率限制,因为我可以干净地将客户端实例插入CoilRetrofit,这样我的所有网络请求都会受到限制,而无需我做任何额外的工作在他们中的任何一个的呼叫站点上:我可以直接拨打enqueue() 而无需考虑。然后重要的是我能够在enqueue()ed 请求上调用cancel()dispose(),这样我就可以避免在用户更改页面时进行不必要的网络请求。

我首先关注this question 的答案,该答案在 OkHttp Interceptor 内部使用 Guava RateLimiter,它运行良好!直到我意识到我需要能够取消挂起的请求,而 Guava 的 RateLimiter 无法做到这一点,因为它在 acquire()s 时阻塞了当前线程,从而阻止了请求立即被取消.

然后我尝试关注this suggestion,您在其中调用Thread.interrupt() 以使被阻止的拦截器恢复,但由于某种原因它无法工作,因为Guava RateLimiters block uninterruptibly。 (注意:使用tryAcquire() 而不是acquire() 然后中断Thread.sleep()ing 不是一个很好的解决方案,因为你不知道要睡多久。)

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

【问题讨论】:

  • 您可以手动控制您自己的 RateLimiting。这是来自 Misk 的一个例子。 github.com/cashapp/misk/blob/…
  • 我必须调查一下,感谢您的指点。我宁愿不必编写自己的 RateLimiter,但如果这是唯一可以做的事情......
  • 您可以在自定义 Call.Factory 中应用速率限制。这将允许您在执行前实现支持取消的自定义调用。
  • 仅供参考,它是刮而不是刮。报废意味着像垃圾一样扔掉东西。
  • 在上下文中我认为只有报废(像垃圾一样扔掉才有意义)

标签: java android kotlin okhttp


【解决方案1】:

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

  • 根本不使用Interceptor,而是允许通过客户端的任何请求照常进行。假设改造请求很少发生,因此我不在乎限制它们。
  • 创建一个包含QueueTimer 的类,该类会定期弹出并运行任务。它并不聪明,但它的效果出奇的好。我的线圈图像请求被放入队列中,以便它们到达最前面时会调用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 之间共享。
  • 技术上可以超过速率限制,但就我而言,这是可以接受的。
  • 您无法在任务到达队列的最前面后取消它们;您只能在它们被执行之前将它们从队列中移除。

【讨论】:

    【解决方案2】:

    创建一个包含您的通信调用的队列,该队列使用计时器/延迟协程,一旦队列为空,就会终止对下一个调用的检查(这样您就不会不必要地运行计时器)。我已经在移动设备和桌面上构建了这样的逻辑......足够简单,可以 IMPL 并完成工作。

    【讨论】:

      【解决方案3】:

      如果你使用的是 Kotlin,那么你可以使用协程和 retoft。 在协程中有 job id ,用于取消您可以使用的任何请求 apiJob?.cancel()

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-09-28
        • 1970-01-01
        • 1970-01-01
        • 2021-01-07
        • 2019-12-08
        • 1970-01-01
        • 1970-01-01
        • 2020-11-04
        相关资源
        最近更新 更多