Skip to content

Commit

Permalink
Merge pull request #475 from Bastian/improve-global-ratelimits
Browse files Browse the repository at this point in the history
 Add ratelimiter to allow custom hard-coded global ratelimits
  • Loading branch information
Bastian committed Nov 22, 2019
2 parents 3d277d6 + 4adb931 commit 917e4b3
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 34 deletions.
8 changes: 8 additions & 0 deletions javacord-api/src/main/java/org/javacord/api/DiscordApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.javacord.api.entity.webhook.Webhook;
import org.javacord.api.listener.GloballyAttachableListenerManager;
import org.javacord.api.util.concurrent.ThreadPool;
import org.javacord.api.util.ratelimit.Ratelimiter;

import java.awt.image.BufferedImage;
import java.io.File;
Expand Down Expand Up @@ -84,6 +85,13 @@ public interface DiscordApi extends GloballyAttachableListenerManager {
*/
AccountType getAccountType();

/**
* Gets the current global ratelimiter.
*
* @return The current global ratelimiter.
*/
Optional<Ratelimiter> getGlobalRatelimiter();

/**
* Creates an invite link for the this bot.
* The method only works for bot accounts!
Expand Down
18 changes: 18 additions & 0 deletions javacord-api/src/main/java/org/javacord/api/DiscordApiBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.javacord.api.listener.GloballyAttachableListener;
import org.javacord.api.util.auth.Authenticator;
import org.javacord.api.util.internal.DelegateFactory;
import org.javacord.api.util.ratelimit.LocalRatelimiter;
import org.javacord.api.util.ratelimit.Ratelimiter;

import java.net.Proxy;
import java.net.ProxySelector;
Expand Down Expand Up @@ -71,6 +73,22 @@ public Collection<CompletableFuture<DiscordApi>> loginShards(int... shards) {
return delegate.loginShards(shards);
}

/**
* Sets a ratelimiter that can be used to control global ratelimits.
*
* <p>By default, no ratelimiter is set, but for large bots or special use-cases, it can be useful to provide
* a ratelimiter with a hardcoded ratelimit to prevent hitting the global ratelimit.
*
* <p>An easy implementation is available with the {@link LocalRatelimiter}.
*
* @param ratelimiter The ratelimiter used to control global ratelimits.
* @return The current instance in order to chain call methods.
*/
public DiscordApiBuilder setGlobalRatelimiter(Ratelimiter ratelimiter) {
delegate.setGlobalRatelimiter(ratelimiter);
return this;
}

/**
* Sets the proxy selector which should be used to determine the proxies that should be used to connect to the
* Discord REST API and web socket.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.javacord.api.DiscordApiBuilder;
import org.javacord.api.listener.GloballyAttachableListener;
import org.javacord.api.util.auth.Authenticator;
import org.javacord.api.util.ratelimit.Ratelimiter;

import java.net.Proxy;
import java.net.ProxySelector;
Expand All @@ -20,6 +21,13 @@
*/
public interface DiscordApiBuilderDelegate {

/**
* Sets a ratelimiter that can be used to control global ratelimits.
*
* @param ratelimiter The ratelimiter used to control global ratelimits.
*/
void setGlobalRatelimiter(Ratelimiter ratelimiter);

/**
* Sets the proxy selector which should be used to determine the proxies that should be used to connect to the
* Discord REST API and websocket.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.javacord.api.util.ratelimit;

/**
* An implementation of {@code Ratelimiter} that allows simple local ratelimits.
*/
public class LocalRatelimiter implements Ratelimiter {

private volatile long nextResetNanos;
private volatile int remainingQuota;

private final int amount;
private final int seconds;

/**
* Creates a new local ratelimiter.
*
* @param amount The amount available per reset interval.
* @param seconds The time to wait until the available quota resets.
*/
public LocalRatelimiter(int amount, int seconds) {
this.amount = amount;
this.seconds = seconds;
}

/**
* Gets the amount available per reset interval.
*
* @return The amount.
*/
public int getAmount() {
return amount;
}

/**
* Gets the time to wait until the available quota resets in seconds.
*
* @return The time to wait until the available quota resets.
*/
public int getSeconds() {
return seconds;
}

/**
* Gets the next time the quota resets.
*
* <p>Use {@link System#nanoTime()} to calculate the absolute difference.
*
* @return The next time the quota resets. Can be in the past.
*/
public long getNextResetNanos() {
return nextResetNanos;
}

/**
* Gets the remaining quota in the current reset interval.
*
* @return The remaining quota.
*/
public int getRemainingQuota() {
return remainingQuota;
}

@Override
public synchronized void requestQuota() throws InterruptedException {
if (remainingQuota <= 0) {
// Wait until a new quota becomes available
long sleepTime;
while ((sleepTime = calculateSleepTime()) > 0) { // Sleep is unreliable, so we have to loop
Thread.sleep(sleepTime);
}
}

// Reset the limit when the last reset timestamp is past
if (System.nanoTime() > nextResetNanos) {
remainingQuota = amount;
nextResetNanos = System.nanoTime() + seconds * 1_000_000_000L;
}

remainingQuota--;
}

private long calculateSleepTime() {
return (nextResetNanos - System.nanoTime()) / 1_000_000;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.javacord.api.util.ratelimit;

/**
* Can be used to implement ratelimits.
*/
public interface Ratelimiter {

/**
* Blocks the requesting thread until a quota becomes available.
*
* @throws InterruptedException if any thread has interrupted the current thread.
* The interrupted status of the current thread is cleared when this exception is
* thrown.
*/
void requestQuota() throws InterruptedException;

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import org.apache.logging.log4j.Logger;
import org.javacord.api.AccountType;
import org.javacord.api.DiscordApi;
import org.javacord.api.DiscordApiBuilder;
import org.javacord.api.internal.DiscordApiBuilderDelegate;
import org.javacord.api.listener.GloballyAttachableListener;
import org.javacord.api.util.auth.Authenticator;
import org.javacord.api.util.ratelimit.Ratelimiter;
import org.javacord.core.util.gateway.DiscordWebSocketAdapter;
import org.javacord.core.util.logging.LoggerUtil;
import org.javacord.core.util.logging.PrivacyProtectionLogger;
Expand Down Expand Up @@ -46,6 +46,11 @@ public class DiscordApiBuilderDelegateImpl implements DiscordApiBuilderDelegate
*/
private static final Logger logger = LoggerUtil.getLogger(DiscordApiBuilderDelegateImpl.class);

/**
* A ratelimiter that is used for global ratelimits.
*/
private volatile Ratelimiter globalRatelimiter;

/**
* The proxy selector which should be used to determine the proxies that should be used to connect to the Discord
* REST API and websocket.
Expand Down Expand Up @@ -161,7 +166,7 @@ public CompletableFuture<DiscordApi> login() {
try (CloseableThreadContext.Instance closeableThreadContextInstance =
CloseableThreadContext.put("shard", Integer.toString(currentShard.get()))) {
new DiscordApiImpl(accountType, token, currentShard.get(), totalShards.get(), waitForServersOnStartup,
proxySelector, proxy, proxyAuthenticator, trustAllCertificates, future, null,
globalRatelimiter, proxySelector, proxy, proxyAuthenticator, trustAllCertificates, future, null,
preparedListeners, preparedUnspecifiedListeners);
}
return future;
Expand Down Expand Up @@ -240,6 +245,11 @@ public Collection<CompletableFuture<DiscordApi>> loginShards(int... shards) {
return result;
}

@Override
public void setGlobalRatelimiter(Ratelimiter ratelimiter) {
globalRatelimiter = ratelimiter;
}

@Override
public void setProxySelector(ProxySelector proxySelector) {
this.proxySelector = proxySelector;
Expand Down Expand Up @@ -337,7 +347,8 @@ public CompletableFuture<Void> setRecommendedTotalShards() {
}

private void setRecommendedTotalShards(CompletableFuture<Void> future) {
DiscordApiImpl api = new DiscordApiImpl(token, proxySelector, proxy, proxyAuthenticator, trustAllCertificates);
DiscordApiImpl api = new DiscordApiImpl(
token, globalRatelimiter, proxySelector, proxy, proxyAuthenticator, trustAllCertificates);
RestRequest<JsonNode> botGatewayRequest = new RestRequest<>(api, RestMethod.GET, RestEndpoint.GATEWAY_BOT);
botGatewayRequest
.execute(RestRequestResult::getJsonBody)
Expand Down
36 changes: 29 additions & 7 deletions javacord-core/src/main/java/org/javacord/core/DiscordApiImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.javacord.api.util.auth.Authenticator;
import org.javacord.api.util.concurrent.ThreadPool;
import org.javacord.api.util.event.ListenerManager;
import org.javacord.api.util.ratelimit.Ratelimiter;
import org.javacord.core.entity.activity.ActivityImpl;
import org.javacord.core.entity.activity.ApplicationInfoImpl;
import org.javacord.core.entity.emoji.CustomEmojiImpl;
Expand Down Expand Up @@ -193,6 +194,11 @@ public class DiscordApiImpl implements DiscordApi, DispatchQueueSelector {
*/
private final boolean waitForServersOnStartup;

/**
* A ratelimiter that is used for global ratelimits.
*/
private final Ratelimiter globalRatelimiter;

/**
* The proxy selector which should be used to determine the proxies that should be used to connect to the Discord
* REST API and websocket.
Expand Down Expand Up @@ -319,16 +325,18 @@ public class DiscordApiImpl implements DiscordApi, DispatchQueueSelector {
* but does not connect to the Discord WebSocket.
*
* @param token The token used to connect without any account type specific prefix.
* @param globalRatelimiter The ratelimiter used for global ratelimits.
* @param proxySelector The proxy selector which should be used to determine the proxies that should be used
* to connect to the Discord REST API and websocket.
* @param proxy The proxy which should be used to connect to the Discord REST API and websocket.
* @param proxyAuthenticator The authenticator that should be used to authenticate against proxies that require
* it.
* @param trustAllCertificates Whether to trust all SSL certificates.
*/
public DiscordApiImpl(String token, ProxySelector proxySelector, Proxy proxy, Authenticator proxyAuthenticator,
boolean trustAllCertificates) {
this(AccountType.BOT, token, 0, 1, false, proxySelector, proxy, proxyAuthenticator, trustAllCertificates, null);
public DiscordApiImpl(String token, Ratelimiter globalRatelimiter, ProxySelector proxySelector, Proxy proxy,
Authenticator proxyAuthenticator, boolean trustAllCertificates) {
this(AccountType.BOT, token, 0, 1, false, globalRatelimiter,
proxySelector, proxy, proxyAuthenticator, trustAllCertificates, null);
}

/**
Expand All @@ -340,6 +348,7 @@ public DiscordApiImpl(String token, ProxySelector proxySelector, Proxy proxy, Au
* @param totalShards The total amount of shards.
* @param waitForServersOnStartup Whether Javacord should wait for all servers
* to become available on startup or not.
* @param globalRatelimiter The ratelimiter used for global ratelimits.
* @param proxySelector The proxy selector which should be used to determine the proxies that should be
* used to connect to the Discord REST API and websocket.
* @param proxy The proxy which should be used to connect to the Discord REST API and websocket.
Expand All @@ -354,14 +363,16 @@ public DiscordApiImpl(
int currentShard,
int totalShards,
boolean waitForServersOnStartup,
Ratelimiter globalRatelimiter,
ProxySelector proxySelector,
Proxy proxy,
Authenticator proxyAuthenticator,
boolean trustAllCertificates,
CompletableFuture<DiscordApi> ready
) {
this(accountType, token, currentShard, totalShards, waitForServersOnStartup, proxySelector, proxy,
proxyAuthenticator, trustAllCertificates, ready, null, Collections.emptyMap(), Collections.emptyList());
this(accountType, token, currentShard, totalShards, waitForServersOnStartup, globalRatelimiter,
proxySelector, proxy, proxyAuthenticator, trustAllCertificates, ready, null,
Collections.emptyMap(), Collections.emptyList());
}

/**
Expand All @@ -373,6 +384,7 @@ public DiscordApiImpl(
* @param totalShards The total amount of shards.
* @param waitForServersOnStartup Whether Javacord should wait for all servers
* to become available on startup or not.
* @param globalRatelimiter The ratelimiter used for global ratelimits.
* @param proxySelector The proxy selector which should be used to determine the proxies that should be
* used to connect to the Discord REST API and websocket.
* @param proxy The proxy which should be used to connect to the Discord REST API and websocket.
Expand All @@ -388,14 +400,16 @@ private DiscordApiImpl(
int currentShard,
int totalShards,
boolean waitForServersOnStartup,
Ratelimiter globalRatelimiter,
ProxySelector proxySelector,
Proxy proxy,
Authenticator proxyAuthenticator,
boolean trustAllCertificates,
CompletableFuture<DiscordApi> ready,
Dns dns) {
this(accountType, token, currentShard, totalShards, waitForServersOnStartup, proxySelector, proxy,
proxyAuthenticator, trustAllCertificates, ready, dns, Collections.emptyMap(), Collections.emptyList());
this(accountType, token, currentShard, totalShards, waitForServersOnStartup, globalRatelimiter,
proxySelector, proxy, proxyAuthenticator, trustAllCertificates, ready, dns, Collections.emptyMap(),
Collections.emptyList());
}

/**
Expand All @@ -406,6 +420,7 @@ private DiscordApiImpl(
* @param totalShards The total amount of shards.
* @param waitForServersOnStartup Whether Javacord should wait for all servers
* to become available on startup or not.
* @param globalRatelimiter The ratelimiter used for global ratelimits.
* @param proxySelector The proxy selector which should be used to determine the proxies that should be
* used to connect to the Discord REST API and websocket.
* @param proxy The proxy which should be used to connect to the Discord REST API and websocket.
Expand All @@ -424,6 +439,7 @@ public DiscordApiImpl(
int currentShard,
int totalShards,
boolean waitForServersOnStartup,
Ratelimiter globalRatelimiter,
ProxySelector proxySelector,
Proxy proxy,
Authenticator proxyAuthenticator,
Expand All @@ -439,6 +455,7 @@ public DiscordApiImpl(
this.currentShard = currentShard;
this.totalShards = totalShards;
this.waitForServersOnStartup = waitForServersOnStartup;
this.globalRatelimiter = globalRatelimiter;
this.proxySelector = proxySelector;
this.proxy = proxy;
this.proxyAuthenticator = proxyAuthenticator;
Expand Down Expand Up @@ -1111,6 +1128,11 @@ public AccountType getAccountType() {
return accountType;
}

@Override
public Optional<Ratelimiter> getGlobalRatelimiter() {
return Optional.ofNullable(globalRatelimiter);
}

@Override
public void setMessageCacheSize(int capacity, int storageTimeInSeconds) {
this.defaultMessageCacheCapacity = capacity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ public RestRequestInformation asRestRequestInformation() {
* @throws Exception If something went wrong while executing the request.
*/
public RestRequestResult executeBlocking() throws Exception {
api.getGlobalRatelimiter().ifPresent(ratelimiter -> {
try {
ratelimiter.requestQuota();
} catch (InterruptedException e) {
logger.warn("Encountered unexpected ratelimiter interrupt", e);
}
});
Request.Builder requestBuilder = new Request.Builder();
HttpUrl.Builder httpUrlBuilder = endpoint.getOkHttpUrl(urlParameters).newBuilder();
queryParameters.forEach(httpUrlBuilder::addQueryParameter);
Expand Down

0 comments on commit 917e4b3

Please sign in to comment.