diff --git a/Java/src/main/java/net/hypixel/api/HypixelAPI.java b/Java/src/main/java/net/hypixel/api/HypixelAPI.java index bb5835a9..faf0bbdf 100644 --- a/Java/src/main/java/net/hypixel/api/HypixelAPI.java +++ b/Java/src/main/java/net/hypixel/api/HypixelAPI.java @@ -12,6 +12,7 @@ import net.hypixel.api.reply.*; import net.hypixel.api.reply.skyblock.*; import net.hypixel.api.util.GameType; +import net.hypixel.api.util.RateLimiter; import net.hypixel.api.util.ResourceType; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; @@ -25,6 +26,7 @@ import java.util.concurrent.Executors; public class HypixelAPI { + private static final int DEFAULT_MAX_REQUESTS_PER_MINUTE = 120; private static final Gson GSON = new GsonBuilder() .registerTypeAdapter(UUID.class, new UUIDTypeAdapter()) @@ -40,19 +42,33 @@ public class HypixelAPI { private final ExecutorService executorService; private final HttpClient httpClient; + private final RateLimiter rateLimiter; public HypixelAPI(UUID apiKey) { this.apiKey = apiKey; this.executorService = Executors.newCachedThreadPool(); this.httpClient = HttpClientBuilder.create().build(); + this.rateLimiter = new RateLimiter(DEFAULT_MAX_REQUESTS_PER_MINUTE); } /** - * Shuts down the internal executor service + * Set how many requests this API instance is allowed to make in one minute. + *

If more requests attempt to pass past this limit in one minute, + * they will need to wait for the next minute. Default is 120. + * + * @param limitPerMinute The new limit. + */ + public void setRateLimit(int limitPerMinute) { + rateLimiter.setRate(limitPerMinute); + } + + /** + * Shuts down the internal executor service and rate limiter service. */ public void shutdown() { executorService.shutdown(); + rateLimiter.shutdown(); } /** @@ -246,6 +262,8 @@ private CompletableFuture get(Class clazz, Strin executorService.submit(() -> { try { + rateLimiter.beforeAction(); + R response = httpClient.execute(new HttpGet(url.toString()), obj -> { String content = EntityUtils.toString(obj.getEntity(), "UTF-8"); if (clazz == ResourceReply.class) { diff --git a/Java/src/main/java/net/hypixel/api/util/RateLimiter.java b/Java/src/main/java/net/hypixel/api/util/RateLimiter.java new file mode 100644 index 00000000..088d8e3a --- /dev/null +++ b/Java/src/main/java/net/hypixel/api/util/RateLimiter.java @@ -0,0 +1,62 @@ +package net.hypixel.api.util; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +import static java.util.concurrent.TimeUnit.MINUTES; + +public class RateLimiter { + private final Object lock = new Object(); + + private final ScheduledExecutorService resetter = Executors.newSingleThreadScheduledExecutor(); + private volatile int limitPerMinute; + private int actionsThisMinute; + + /** + * @param limitPerMinute Maximum amount of times {@link RateLimiter#beforeAction()} can be called in the same minute + * before it begins blocking threads until the next minute. + */ + public RateLimiter(int limitPerMinute) { + this.limitPerMinute = limitPerMinute; + resetter.scheduleAtFixedRate(this::reset, 1, 1, MINUTES); + } + + private void reset() { + synchronized ( lock ) { + actionsThisMinute = 0; + lock.notifyAll(); + } + } + + /** + * Set the action queue limit to be used by {@link RateLimiter#beforeAction()}. + * + * @param limitPerMinute The new limit. + */ + public void setRate(int limitPerMinute) { + this.limitPerMinute = limitPerMinute; + } + + /** + * Blocks the current thread in the event that the action queue has been filled for this minute. + * Unblocks when the queue refreshes. + * + * @throws InterruptedException If interrupted while waiting. + */ + public void beforeAction() throws InterruptedException { + synchronized ( lock ) { + while (actionsThisMinute >= limitPerMinute) { + lock.wait(); + } + + actionsThisMinute++; + } + } + + /** + * Shut down the internal executor service. + */ + public void shutdown() { + resetter.shutdown(); + } +}