taycaldwell / riot-api-java

Riot Games API Java Library
http://taycaldwell.github.io/riot-api-java/
Apache License 2.0
192 stars 73 forks source link

API request limiter and queue #77

Open briceambrosiak opened 8 years ago

briceambrosiak commented 8 years ago

I've just discovered some limitation in the Riot API, which is that you can be banned temporarily/permanently if you hit the number of requests limit on your API key (I thought they would just say "Forbidden, try later").

I will then have to implement some sort of queue, in which all the API requests will be put before even contacting your library (some kind of proxy).

I was wondering in which measure your library could already implement a queue, with some initial methods to parametrize the user's key limits (X requests per 10s, Y requests per 10m, ...) ? (or even auto-discovery with X-Rate-Limit headers returned by the API).

adobito commented 8 years ago

You can be banned if you exceed your rate limits constantly, but if you back off for the amount of time specified in the retry after header. You could go fancier using the Rate Limit Count Header too, but unless you're running something intense that requires the utmost optimization on your end, it is very likely overkill.

briceambrosiak commented 8 years ago

Actually, I'm using quite intensively the API in batch mode, querying many game data etc. Some wrapper I made around your API to overcome this :

package me.biru.labs.service;

import me.biru.labs.util.Utils;
import net.rithms.riot.api.RiotApi;
import net.rithms.riot.api.RiotApiException;
import net.rithms.riot.dto.Game.RecentGames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

/**
 * Wrapper for the Riot API counting requests and limiting it withing the rate limit.
 * This will prevent an overuse of the API and receiving "forbidden" status from the API.
 *
 * @author Brice Ambrosiak
 */
@Service
public class RiotApiWrapper {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    /** Riot API service */
    private RiotApi riotApi;

    /** Rate limits to the Riot API */
    private List<RateLimitDto> rateLimits;

    @PostConstruct
    public void init() {
        rateLimits = new ArrayList<>();
        rateLimits.add(new RateLimitDto(10, TimeUnit.SECONDS, 10));
        rateLimits.add(new RateLimitDto(500, TimeUnit.MINUTES, 10));
    }

    //TODO Implement all API requests
    public RecentGames getRecentGames(long summonerId) throws RiotApiException {
        registerRequest();
        return riotApi.getRecentGames(summonerId);
    }

    public long fakeRequest() {
        log.info("Fake request.");
        registerRequest();
        return System.currentTimeMillis();
    }

    /**
     * Registers the request made to the API
     * Waits until the request can be sent (within rate limits) and will sleep 1 second between each wait
     * Also update and clean the queues
     */
    private void registerRequest() {
        log.debug("Registering request...");

        // Wait until request can be made
        while (!canSendRequest()) {
            Utils.sleep(1);
            log.debug("Waiting 1 second...");
        }

        // Add the new request as timestamp
        long t = System.currentTimeMillis();
        for (RateLimitDto rateLimit : rateLimits) {
            rateLimit.getLastRequests().add(t);
        }

        log.debug("Request registered.");
    }

    /**
     * Checks if the request can be sent to the API, and cleans old requests
     * Will check each rate limit declared
     *
     * @return True if the request can be sent
     */
    private boolean canSendRequest() {
        log.debug("Checking if the request can be sent...");
        long tNow = System.currentTimeMillis();

        for (RateLimitDto rateLimit : rateLimits) {
            cleanOldRequests(tNow, rateLimit);
        }

        return checkRateLimits();
    }

    /**
     * Cleans old requests of the given rate limit, given the current timestamp considered
     *
     * @param tNow Current timestamp
     * @param rateLimit Rate limit for which to clean old out-dated requests
     */
    private void cleanOldRequests(long tNow, RateLimitDto rateLimit) {
        // Clean old requests
        log.debug("{} hits in the last {}. Cleaning old requests...", rateLimit.getLastRequests().size(), rateLimit);
        boolean endLoop = false;
        while (!rateLimit.getLastRequests().isEmpty() && !endLoop) {
            long tLastRequest = rateLimit.getLastRequests().peek();
            long tDiff = rateLimit.getTimeUnit().convert(tNow - tLastRequest, TimeUnit.MILLISECONDS);
            // It has been more than 10 seconds since last request was done, clean it
            if (tDiff > rateLimit.getIntervalInGivenTimeUnit()) {
                long tClean = rateLimit.getLastRequests().poll();
                log.debug("Cleaned request {} ({} {} ago).", tClean, tDiff, rateLimit.getTimeUnit());
            } else {
                // We are trying to do a request withing the 10s interval, it's still forbidden, nothing to clean
                endLoop = true;
            }
        }
    }

    /**
     * Check all rate limits defined, and tell if they are over or not
     *
     * @return False at the first rate limit exceeded, True otherwise
     */
    private boolean checkRateLimits() {
        for (RateLimitDto rateLimit : rateLimits) {
            log.debug("Checking {} limit...", rateLimit);
            if (rateLimit.getLastRequests().size() >= rateLimit.getMaxRequests()) {
                log.debug("{} limit exceeded.", rateLimit);
                return false;
            }
        }
        log.debug("Rate limits are OK.");
        return true;
    }
}

/**
 * Data object holding a rate limit information
 */
class RateLimitDto {
    /** Queue storing all previous requests made to the API */
    private Queue<Long> lastRequests;
    /** Maximum number of requests that can be made in the given interval */
    private int maxRequests;
    /** Time unit of the interval */
    private TimeUnit timeUnit;
    /** Interval for which the maximum number of requests are counted, in time unit */
    private int intervalInGivenTimeUnit;

    public RateLimitDto(int maxRequests, TimeUnit timeUnit, int intervalInGivenTimeUnit) {
        this.lastRequests = new LinkedList<>();
        this.maxRequests = maxRequests;
        this.timeUnit = timeUnit;
        this.intervalInGivenTimeUnit = intervalInGivenTimeUnit;
    }

    public Queue<Long> getLastRequests() {
        return lastRequests;
    }
    public void setLastRequests(Queue<Long> lastRequests) {
        this.lastRequests = lastRequests;
    }
    public int getMaxRequests() {
        return maxRequests;
    }
    public void setMaxRequests(int maxRequests) {
        this.maxRequests = maxRequests;
    }
    public TimeUnit getTimeUnit() {
        return timeUnit;
    }
    public void setTimeUnit(TimeUnit timeUnit) {
        this.timeUnit = timeUnit;
    }
    public int getIntervalInGivenTimeUnit() {
        return intervalInGivenTimeUnit;
    }
    public void setIntervalInGivenTimeUnit(int intervalInGivenTimeUnit) {
        this.intervalInGivenTimeUnit = intervalInGivenTimeUnit;
    }

    @Override
    public String toString() {
        return getIntervalInGivenTimeUnit() + " " + getTimeUnit();
    }
}