package com.fullcontact.api.libs.fullcontact4j.http;
import com.fullcontact.api.libs.fullcontact4j.FCConstants;
import com.fullcontact.api.libs.fullcontact4j.FullContact;
import com.fullcontact.api.libs.fullcontact4j.FullContactApi;
import com.fullcontact.api.libs.fullcontact4j.Utils;
import com.fullcontact.api.libs.fullcontact4j.enums.RateLimiterConfig;
import com.fullcontact.api.libs.fullcontact4j.guava.SmoothRateLimiter;
import com.fullcontact.api.libs.fullcontact4j.http.person.PersonRequest;
import com.fullcontact.api.libs.fullcontact4j.http.person.PersonResponse;
import retrofit.client.Header;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* This class handles requests made by the client.
* When a request is made, it is sent to an ExecutorService which
* accounts for rate limiting and then sends the request.
*/
public class RequestExecutorHandler implements FCRequestHandler {
// how often to check for a rate limit change
private static final long RATE_LIMIT_CHECK_INTERVAL_MS = TimeUnit.MINUTES.convert(5, TimeUnit.MILLISECONDS);
//will execute the requests on a separate thread.
protected final ExecutorService executorService;
//if not null, will limit the request rate.
private SmoothRateLimiter.SmoothBursty rateLimiter;
private double apiKeyRequestsPerSecond;
private FCRateLimits lastKnownRateLimits;
private volatile long lastRateLimitCheck = 0;
private RequestDebtTracker requestDebtTracker = new RequestDebtTracker();
public RequestExecutorHandler(RateLimiterConfig rateLimiterConfig, ExecutorService executorService) {
this.executorService = executorService;
apiKeyRequestsPerSecond = rateLimiterConfig.getInitReqsPerSec();
rateLimiter = rateLimiterConfig.createRateLimiter();
}
/**
* If the check interval time has passed, update the rate limit
*/
private void updateRateLimit() {
rateLimiter.setRate(apiKeyRequestsPerSecond);
lastRateLimitCheck = System.currentTimeMillis();
}
public synchronized void notifyRateLimits(FCResponse res, FCRateLimits rateLimits) {
if(!(res instanceof PersonResponse)) {
return; // not person api headers, ignore
}
int requestsRemaining = rateLimits.getRequestsRemaining();
int secondsToReset = rateLimits.getSecondsToReset();
apiKeyRequestsPerSecond = rateLimits.getMaxRequestsPerSecond();
lastKnownRateLimits = rateLimits;
if (shouldUpdateRateLimit()) {
updateRateLimit();
}
//are we out of requests for this session?
if(requestsRemaining <= apiKeyRequestsPerSecond && lastKnownRateLimits.getSecondsToReset() != 0) {
Utils.info("To keep in line with rate limit headers, FC4J is waiting " + secondsToReset + "s " +
"to the new rate limit period.");
requestDebtTracker.registerDebt(secondsToReset * 1000);
}
}
/**
* Has RATE_LIMIT_CHECK time passed since we last made a rate limit update?
*/
private boolean shouldUpdateRateLimit() {
return System.currentTimeMillis() - lastRateLimitCheck > RATE_LIMIT_CHECK_INTERVAL_MS;
}
public <T extends FCResponse> void sendRequestAsync(final FullContactApi api, final FCRequest<T> req,
final FCRetrofitCallback<T> callback) {
executorService.execute(new Runnable() {
@Override
public void run() {
// account for rate limits for Person API only
if(req instanceof PersonRequest) {
//wait until this request would be made within API key limits
waitForPermit();
//wait until this request would be made within rate limit header limits
requestDebtTracker.consumeDebt();
}
Utils.verbose("Sending a new asynchronous " + req.getClass().getSimpleName());
req.makeRequest(api, callback);
}
});
}
protected void waitForPermit() {
if(rateLimiter != null) {
Utils.verbose("Waiting for ratelimiter to allow a request... (" + rateLimiter.getRate() + " reqs/s)");
rateLimiter.acquire();
}
}
public void shutdown() {
executorService.shutdown();
}
/**
* FullContact will provide headers with the amount of requests remaining in the current rate limit session.
* If we have 0 requests remaining in the current period we can have the client
* sleep the client until we would not exceed rate limit.
*/
private class RequestDebtTracker {
/**
* The amount, in milliseconds, to sleep.
*/
private volatile int debt = 0;
/**
* Registers the amount of debt to consume (as long as it's more debt than registered right now).
*/
public synchronized void registerDebt(int debt) {
//disable creating permits until this debt is paid off
RequestExecutorHandler.this.rateLimiter.disableBursting();
if (this.debt < debt) {
Utils.verbose("Registering debt of " + debt + "ms to account for rate limit headers.");
this.debt = debt;
}
}
/**
* Sleep the current thread until the debt is consumed.
* Other threads will block until this debt is consumed.
* If more debt is registered while this is being consumed, it'll consume that, as well.
*/
private synchronized void consumeDebt() {
while (debt > 0) {
int copy = debt;
debt = 0;
try {
Utils.verbose(System.currentTimeMillis() + " Consuming " + copy + "ms of request debt.");
Thread.sleep(copy);
} catch (InterruptedException e) {
Utils.info("[WARN] Interrupted while consuming request debt! Exception: " + e.getMessage());
}
}
//allow creating permits
RequestExecutorHandler.this.rateLimiter.enableBursting(apiKeyRequestsPerSecond);
}
}
}