Skip to content

Commit

Permalink
Fix #1273 AsyncMethodsRateLimiter does not handle ratelimitted errors…
Browse files Browse the repository at this point in the history
… properly (#1274)
  • Loading branch information
seratch authored Jan 24, 2024
1 parent 84723a5 commit 36e41c9
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.slack.api.rate_limits.WaitTime;
import com.slack.api.rate_limits.WaitTimeCalculator;
import com.slack.api.rate_limits.metrics.MetricsDatastore;
import com.slack.api.rate_limits.metrics.RequestPace;
import lombok.extern.slf4j.Slf4j;

import java.util.Optional;
Expand All @@ -19,6 +20,7 @@ public class AsyncMethodsRateLimiter implements RateLimiter {
private final MetricsDatastore metricsDatastore;
private final MethodsCustomRateLimitResolver customRateLimitResolver;
private final WaitTimeCalculator waitTimeCalculator;
private final String executorName;

public MetricsDatastore getMetricsDatastore() {
return metricsDatastore;
Expand All @@ -28,10 +30,17 @@ public AsyncMethodsRateLimiter(MethodsConfig config) {
this.metricsDatastore = config.getMetricsDatastore();
this.customRateLimitResolver = config.getCustomRateLimitResolver();
this.waitTimeCalculator = new MethodsWaitTimeCalculator(config);
this.executorName = config.getExecutorName();
}

@Override
public WaitTime acquireWaitTime(String teamId, String methodName) {
Optional<Long> rateLimitedEpochMillis = waitTimeCalculator
.getRateLimitedMethodRetryEpochMillis(executorName, teamId, methodName);
if (rateLimitedEpochMillis.isPresent()) {
long millisToWait = rateLimitedEpochMillis.get() - System.currentTimeMillis();
return new WaitTime(millisToWait, RequestPace.RateLimited);
}
return waitTimeCalculator.calculateWaitTime(
teamId,
methodName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package test_locally.api.methods;

import com.slack.api.Slack;
import com.slack.api.SlackConfig;
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.rate_limits.metrics.MetricsDatastore;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import util.MockSlackApiServer;

import java.util.concurrent.TimeUnit;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static util.MockSlackApi.RateLimitedToken;

@Slf4j
public class RateLimitedTest {

MockSlackApiServer server = new MockSlackApiServer();
SlackConfig config = new SlackConfig();
Slack slack = Slack.getInstance(config);

String executorName = RateLimitedTest.class.getCanonicalName();

@Before
public void setup() throws Exception {
server.start();
config.getMethodsConfig().setExecutorName(executorName);
config.synchronizeMetricsDatabases();
config.setMethodsEndpointUrlPrefix(server.getMethodsEndpointPrefix());
}

@After
public void tearDown() throws Exception {
server.stop();
}

@Test
public void sync() throws Exception {
MethodsClient client = slack.methods(RateLimitedToken);
try {
client.usersList(r -> r);
} catch (SlackApiException e) {
assertThat(e.getResponse().code(), is(429));
assertThat(e.getResponse().header("Retry-After"), is("3"));
MetricsDatastore datastore = config.getMethodsConfig().getMetricsDatastore();
log.debug("stats: {}", datastore.getAllStats());

Integer numOfRequests = datastore.getNumberOfLastMinuteRequests(
executorName,
"T1234567",
"users.list");
assertThat(numOfRequests, is(1));

Long millisToResume = datastore.getRateLimitedMethodRetryEpochMillis(
executorName,
"T1234567",
"users.list");
assertThat(millisToResume, is(greaterThan(0L)));
}
}

@Test
public void async() throws Exception {
try {
slack.methodsAsync(RateLimitedToken).usersList(r -> r).get(2, TimeUnit.SECONDS);
} catch (Exception ee) {
MetricsDatastore datastore = config.getMethodsConfig().getMetricsDatastore();
log.debug("stats: {}", datastore.getAllStats());

Integer numOfRequests = datastore.getNumberOfLastMinuteRequests(
executorName,
"T1234567",
"users.list");
assertThat(numOfRequests, is(1));

Long millisToResume = datastore.getRateLimitedMethodRetryEpochMillis(
executorName,
"T1234567",
"users.list");
assertThat(millisToResume, is(greaterThan(0L)));
}
}

}
23 changes: 23 additions & 0 deletions slack-api-client/src/test/java/util/MockSlackApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class MockSlackApi extends HttpServlet {
public static final String ValidFunctionToken = "xwfp-this-is-valid";
public static final String ExpiredToken = "xoxb-this-is-expired";
public static final String InvalidToken = "xoxb-this-is-INVALID";
public static final String RateLimitedToken = "xoxb-this-is-ratelimited";

private final FileReader reader = new FileReader("../json-logs/samples/api/");

Expand All @@ -44,6 +45,28 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I
resp.getWriter().write("{\"ok\":false,\"error\":\"not_authed\"}");
resp.setContentType("application/json");
return;
} else if (authorizationHeader.startsWith("Bearer " + RateLimitedToken)) {
if (methodName.equals("auth.test")) {
String body = "{\n" +
" \"ok\": true,\n" +
" \"url\": \"https://java-slack-sdk-test.slack.com/\",\n" +
" \"team\": \"java-slack-sdk-test\",\n" +
" \"user\": \"test_user\",\n" +
" \"team_id\": \"T1234567\",\n" +
" \"user_id\": \"U1234567\",\n" +
" \"bot_id\": \"B12345678\",\n" +
" \"enterprise_id\": \"E12345678\"\n" +
"}";
resp.setStatus(200);
resp.getWriter().write(body);
resp.setContentType("application/json");
return;
}
resp.setStatus(429);
resp.getWriter().write("{\"ok\":false,\"error\":\"ratelimited\"}");
resp.setHeader("Retry-After", "3");
resp.setContentType("application/json");
return;
} else if (!authorizationHeader.startsWith("Bearer " + ValidToken)
&& !authorizationHeader.startsWith("Bearer " + ValidFunctionToken)) {
resp.setStatus(200);
Expand Down

0 comments on commit 36e41c9

Please sign in to comment.