I'm building a java client interacting with a server that returns responses with rate limit info in the headers:
- rate limit per period (for instance 10 per second)
- remaining limit left until next period
- start of next period (epoch time in seconds)
So I built a rate limiter that doesn't follow a fixed rate, but adapts to the info from the responses. The code looks as follows:
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.ws.rs.core.Response;
public final class BolRateLimiter {
private static final String HEADER_KEY_LIMIT = "X-RateLimit-Limit"; //$NON-NLS-1$
private static final String HEADER_KEY_REMAINING = "X-RateLimit-Remaining"; //$NON-NLS-1$
private static final String HEADER_KEY_RESET = "X-RateLimit-Reset"; //$NON-NLS-1$
private final Semaphore limiter = new Semaphore(1, true);
private final AtomicBoolean init = new AtomicBoolean(true);
private final AtomicBoolean resetting = new AtomicBoolean(false);
public void limit() {
try {
limiter.acquire();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public void read(final Response r) {
if (init.compareAndSet(true, false)) {
final String remainstr = r.getHeaderString(HEADER_KEY_REMAINING);
final int remain = parseInt(remainstr);
limiter.release(remain);
}
if (resetting.compareAndSet(false, true)) {
final String limitstr = r.getHeaderString(HEADER_KEY_LIMIT);
final String resetstr = r.getHeaderString(HEADER_KEY_RESET);
final int limit = parseInt(limitstr);
final long reset = SECONDS.toMillis(parseLong(resetstr));
new Thread(() -> {
try {
final long now = currentTimeMillis();
final long delta = reset - now + 100l;
MILLISECONDS.sleep(delta);
limiter.drainPermits();
limiter.release(limit);
resetting.set(false);
} catch (final InterruptedException e) {
currentThread().interrupt();
}
}).start();
}
}
}
And it's used in for instance the following context:
@Override
public BolProcess checkProcess(final String processid) {
--> limiter1.limit(); //
final Invocation request = endpoints //
.path("process-status") //$NON-NLS-1$
.path(processid) //
.request() //
.header(AUTHORIZATION, tokenprov.getToken()) //
.header(ACCEPT, HeaderAcceptValueJson) //
.buildGet(); //
try (final Response response = request.invoke()) {
--> limiter1.read(response);
if (response.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL))
return response.readEntity(BolProcess.class);
throw response.readEntity(BolEndpointException.class);
}
}
The semaphore starts with 1 permit, so the client can initially send just 1 message before the rate limiter blocks. When the rate limiter reads its first response, the semaphore is instructed to release more permits.
Besides that, the limiter reads from the response header the rate limit as well as the time to reset. It then starts a thread that will reset the semaphore permits once the time to reset passed. The rate limiter then ignores rate limit info from subsequent responses, until thread reset the semaphore.
I need this rate limiter to work in a parallel/concurrent context, like parallel streams. In that context it does not work to read the remaining limit left from a response (except from the first). By the time the rate limiter would read a response and the remaining limit left, that limit is already outdated.
So am I overlooking anything here? I'd be happy to get your feedback. Thanks in advance!