Skip to content

Commit cb77d74

Browse files
authored
Add MethodsCustomRateLimitResolver to enable developers to override the rate limiter's behavior (#1134)
1 parent ba69bbc commit cb77d74

File tree

5 files changed

+166
-5
lines changed

5 files changed

+166
-5
lines changed

slack-api-client/src/main/java/com/slack/api/methods/MethodsConfig.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ public void setCustomThreadPoolSizes(Map<String, Integer> customThreadPoolSizes)
7070
public void setExecutorServiceProvider(ExecutorServiceProvider executorServiceProvider) {
7171
throwException();
7272
}
73+
74+
@Override
75+
public void setCustomRateLimitResolver(MethodsCustomRateLimitResolver customRateLimitResolver) {
76+
throwException();
77+
}
7378
};
7479

7580
@Builder.Default
@@ -108,4 +113,10 @@ public void setExecutorServiceProvider(ExecutorServiceProvider executorServicePr
108113
@Builder.Default
109114
private MetricsDatastore metricsDatastore = new MemoryMetricsDatastore(1);
110115

116+
/**
117+
* Custom handlers for adjusting rate limits.
118+
*/
119+
@Builder.Default
120+
private MethodsCustomRateLimitResolver customRateLimitResolver = MethodsCustomRateLimitResolver.DEFAULT;
121+
111122
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.slack.api.methods;
2+
3+
import java.util.Optional;
4+
5+
/**
6+
* Custom rate limit resolver.
7+
* <p>
8+
* When you need to adjust the rate limits for some reason,
9+
* you can implement this interface and pass it to MethodsConfig#setCustomRateLimitResolver() method.
10+
* With that, you can override the allowed requests per minute for selected methods.
11+
*/
12+
public interface MethodsCustomRateLimitResolver {
13+
14+
/**
15+
* Return a present value only when you want to override the allowed requests per minute.
16+
* Otherwise, returning Optional.empty() will result in falling back to the built-in calculation.
17+
*
18+
* @param teamId the workspace ID
19+
* @param methodName the method name such as "auth.test"
20+
* @return the number of allowed requests per minute
21+
*/
22+
Optional<Integer> getCustomAllowedRequestsPerMinute(String teamId, String methodName);
23+
24+
/**
25+
* Return a present value only when you want to override the allowed requests per minute.
26+
* Otherwise, returning Optional.empty() will result in falling back to the built-in calculation.
27+
*
28+
* @param teamId the workspace ID
29+
* @param channel either a channel ID or channel name
30+
* @return the number of allowed requests per minute
31+
*/
32+
Optional<Integer> getCustomAllowedRequestsForChatPostMessagePerMinute(String teamId, String channel);
33+
34+
class Default implements MethodsCustomRateLimitResolver {
35+
@Override
36+
public Optional<Integer> getCustomAllowedRequestsPerMinute(String teamId, String methodName) {
37+
return Optional.empty();
38+
}
39+
40+
@Override
41+
public Optional<Integer> getCustomAllowedRequestsForChatPostMessagePerMinute(String teamId, String channel) {
42+
return Optional.empty();
43+
}
44+
}
45+
46+
MethodsCustomRateLimitResolver DEFAULT = new Default();
47+
48+
}

slack-api-client/src/main/java/com/slack/api/methods/impl/AsyncMethodsRateLimiter.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.slack.api.methods.impl;
22

33
import com.slack.api.methods.MethodsConfig;
4+
import com.slack.api.methods.MethodsCustomRateLimitResolver;
45
import com.slack.api.methods.MethodsRateLimits;
56
import com.slack.api.rate_limits.RateLimiter;
67
import com.slack.api.rate_limits.WaitTime;
@@ -10,10 +11,13 @@
1011

1112
import java.util.Optional;
1213

14+
import static com.slack.api.methods.MethodsRateLimitTier.SpecialTier_chat_postMessage;
15+
1316
@Slf4j
1417
public class AsyncMethodsRateLimiter implements RateLimiter {
1518

1619
private final MetricsDatastore metricsDatastore;
20+
private final MethodsCustomRateLimitResolver customRateLimitResolver;
1721
private final WaitTimeCalculator waitTimeCalculator;
1822

1923
public MetricsDatastore getMetricsDatastore() {
@@ -22,6 +26,7 @@ public MetricsDatastore getMetricsDatastore() {
2226

2327
public AsyncMethodsRateLimiter(MethodsConfig config) {
2428
this.metricsDatastore = config.getMetricsDatastore();
29+
this.customRateLimitResolver = config.getCustomRateLimitResolver();
2530
this.waitTimeCalculator = new MethodsWaitTimeCalculator(config);
2631
}
2732

@@ -30,15 +35,32 @@ public WaitTime acquireWaitTime(String teamId, String methodName) {
3035
return waitTimeCalculator.calculateWaitTime(
3136
teamId,
3237
methodName,
33-
waitTimeCalculator.getAllowedRequestsPerMinute(MethodsRateLimits.lookupRateLimitTier(methodName))
38+
getAllowedRequestsPerMinute(teamId, methodName)
3439
);
3540
}
3641

42+
public int getAllowedRequestsPerMinute(String teamId, String methodName) {
43+
Optional<Integer> custom = customRateLimitResolver.getCustomAllowedRequestsPerMinute(teamId, methodName);
44+
if (custom.isPresent()) {
45+
return custom.get();
46+
}
47+
return waitTimeCalculator.getAllowedRequestsPerMinute(MethodsRateLimits.lookupRateLimitTier(methodName));
48+
}
49+
50+
public int getAllowedRequestsForChatPostMessagePerMinute(String teamId, String channel) {
51+
Optional<Integer> custom = customRateLimitResolver.getCustomAllowedRequestsForChatPostMessagePerMinute(teamId, channel);
52+
if (custom.isPresent()) {
53+
return custom.get();
54+
}
55+
return waitTimeCalculator.getAllowedRequestsPerMinute(SpecialTier_chat_postMessage);
56+
}
57+
3758
@Override
3859
public WaitTime acquireWaitTimeForChatPostMessage(String teamId, String channel) {
3960
return waitTimeCalculator.calculateWaitTimeForChatPostMessage(
4061
teamId,
41-
channel
62+
channel,
63+
getAllowedRequestsForChatPostMessagePerMinute(teamId, channel)
4264
);
4365
}
4466

slack-api-client/src/main/java/com/slack/api/rate_limits/WaitTimeCalculator.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,19 @@ public WaitTime calculateWaitTime(String teamId, String key, int allowedRequests
4444
}
4545
}
4646

47+
/**
48+
* @deprecated Use #calculateWaitTimeForChatPostMessage(String, String, int) instead
49+
*/
50+
@Deprecated
4751
public WaitTime calculateWaitTimeForChatPostMessage(String teamId, String channel) {
52+
return calculateWaitTimeForChatPostMessage(teamId, channel, getAllowedRequestsPerMinute(SpecialTier_chat_postMessage));
53+
}
54+
55+
public WaitTime calculateWaitTimeForChatPostMessage(String teamId, String channel, int allowedRequests) {
4856
return calculateWaitTime(
4957
teamId,
5058
Methods.CHAT_POST_MESSAGE + "_" + channel,
51-
getAllowedRequestsPerMinute(SpecialTier_chat_postMessage)
59+
allowedRequests
5260
);
5361
}
5462

@@ -76,8 +84,8 @@ private static boolean isTooFastPaced(LastMinuteRequests lastMinuteRequests, int
7684
}
7785

7886
public Integer getAllowedRequestsPerMinute(MethodsRateLimitTier tier) {
79-
Integer allowedRequestsForOneNode = MethodsRateLimitTier.getAllowedRequestsPerMinute(tier);
80-
return allowedRequestsForOneNode / getNumberOfNodes();
87+
Integer allowedRequestsPerMinute = MethodsRateLimitTier.getAllowedRequestsPerMinute(tier);
88+
return allowedRequestsPerMinute / getNumberOfNodes();
8189
}
8290

8391
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package test_locally.api.methods;
2+
3+
import com.slack.api.Slack;
4+
import com.slack.api.SlackConfig;
5+
import com.slack.api.methods.AsyncMethodsClient;
6+
import com.slack.api.methods.MethodsCustomRateLimitResolver;
7+
import org.junit.After;
8+
import org.junit.Before;
9+
import org.junit.Test;
10+
import util.MockSlackApiServer;
11+
12+
import java.util.Optional;
13+
14+
import static org.hamcrest.CoreMatchers.is;
15+
import static org.hamcrest.MatcherAssert.assertThat;
16+
import static org.hamcrest.Matchers.greaterThan;
17+
import static util.MockSlackApi.ValidToken;
18+
19+
public class CustomRateLimitsTest {
20+
21+
MockSlackApiServer server = new MockSlackApiServer();
22+
Slack slack;
23+
24+
static int customLimit = 30;
25+
26+
static class MyCustomRateLimitResolver implements MethodsCustomRateLimitResolver {
27+
28+
@Override
29+
public Optional<Integer> getCustomAllowedRequestsPerMinute(String teamId, String methodName) {
30+
if (methodName.equals("auth.test")) {
31+
return Optional.of(customLimit);
32+
}
33+
return Optional.empty();
34+
}
35+
36+
@Override
37+
public Optional<Integer> getCustomAllowedRequestsForChatPostMessagePerMinute(String teamId, String channel) {
38+
return Optional.empty();
39+
}
40+
}
41+
42+
@Before
43+
public void setup() throws Exception {
44+
server.start();
45+
SlackConfig config = new SlackConfig();
46+
// Customize the rate limits
47+
config.getMethodsConfig().setCustomRateLimitResolver(new MyCustomRateLimitResolver());
48+
config.setMethodsEndpointUrlPrefix(server.getMethodsEndpointPrefix());
49+
slack = Slack.getInstance(config);
50+
}
51+
52+
@After
53+
public void tearDown() throws Exception {
54+
server.stop();
55+
}
56+
57+
@Test
58+
public void burstTest() throws Exception {
59+
AsyncMethodsClient client = slack.methodsAsync(ValidToken);
60+
client.authTest(r -> r).get();
61+
client.authTest(r -> r).get();
62+
client.authTest(r -> r).get();
63+
client.authTest(r -> r).get();
64+
65+
long startMillis = System.currentTimeMillis();
66+
// With the default settings, this call is on the "safe" pace, so spentMillis will be 15L or so
67+
client.authTest(r -> r).get();
68+
long spentMillis = System.currentTimeMillis() - startMillis;
69+
assertThat(spentMillis, is(greaterThan(500L )));
70+
}
71+
72+
}

0 commit comments

Comments
 (0)