I'm writing an Android app that makes frequent requests to a REST API service. This service has a hard request limit of 2 requests per second, after which it will return HTTP 503 with no other information. I'd like to be a good developer and rate limit my app to stay in compliance with the service's requirements (i.e, not retry-spamming the service until my requests succeed) but it's proving difficult to do.
I'm trying to rate limit OkHttpClient specifically, because I can cleanly slot an instance of a client into both Coil and Retrofit so that all my network requests are limited without me having to do any extra work at the callsites for either of them: I can just call enqueue() without thinking about it. And then it's important that I be able to call cancel() or dispose() on the enqueue()ed requests so that I can avoid doing unnecessary network requests when the user changes the page, for example.
I started by following an answer to this question that uses a Guava RateLimiter inside of an OkHttp Interceptor, and it worked perfectly! Up until I realized that I needed to be able to cancel pending requests, and you can't do that with Guava's RateLimiter, because it blocks the current thread when it acquire()s, which then prevents the request from being cancelled immediately.
I then tried following this suggestion, where you call Thread.interrupt() to get the blocked interceptor to resume, but it won't work because Guava RateLimiters block uninterruptibly for some reason. (Note: doing tryAcquire() instead of acquire() and then interruptibly Thread.sleep()ing isn't a great solution, because you can't know how long to sleep for.)
So then I started thinking about scrapping the Guava solution and implementing a custom ExecutorService that would hold the requests in a queue that would be periodically dispatched by a timer, but it seems like a lot of complicated work for something that may or may not work and I'm way off into the weeds now. Is there a better or simpler way to do what I want?