Skip to content

Commit f0ed76c

Browse files
committed
Implement rate-limiting
1 parent 9f83b38 commit f0ed76c

5 files changed

Lines changed: 926 additions & 6 deletions

File tree

extractor/src/test/java/org/schabi/newpipe/downloader/DownloaderTestImpl.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.schabi.newpipe.downloader;
22

3+
import org.schabi.newpipe.downloader.ratelimiting.RateLimitedClientWrapper;
34
import org.schabi.newpipe.extractor.downloader.Downloader;
45
import org.schabi.newpipe.extractor.downloader.Request;
56
import org.schabi.newpipe.extractor.downloader.Response;
@@ -24,10 +25,11 @@ public final class DownloaderTestImpl extends Downloader {
2425
private static final String USER_AGENT
2526
= "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0";
2627
private static DownloaderTestImpl instance;
27-
private final OkHttpClient client;
28+
private final RateLimitedClientWrapper clientWrapper;
2829

2930
private DownloaderTestImpl(final OkHttpClient.Builder builder) {
30-
this.client = builder.readTimeout(30, TimeUnit.SECONDS).build();
31+
this.clientWrapper = new RateLimitedClientWrapper(
32+
builder.readTimeout(30, TimeUnit.SECONDS).build());
3133
}
3234

3335
/**
@@ -66,22 +68,22 @@ public Response execute(@Nonnull final Request request)
6668
.method(httpMethod, requestBody).url(url)
6769
.addHeader("User-Agent", USER_AGENT);
6870

69-
for (Map.Entry<String, List<String>> pair : headers.entrySet()) {
71+
for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
7072
final String headerName = pair.getKey();
7173
final List<String> headerValueList = pair.getValue();
7274

7375
if (headerValueList.size() > 1) {
7476
requestBuilder.removeHeader(headerName);
75-
for (String headerValue : headerValueList) {
77+
for (final String headerValue : headerValueList) {
7678
requestBuilder.addHeader(headerName, headerValue);
7779
}
7880
} else if (headerValueList.size() == 1) {
7981
requestBuilder.header(headerName, headerValueList.get(0));
8082
}
81-
8283
}
8384

84-
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
85+
final okhttp3.Response response =
86+
clientWrapper.executeRequestWithLimit(requestBuilder.build());
8587

8688
if (response.code() == 429) {
8789
response.close();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.schabi.newpipe.downloader.ratelimiting;
2+
3+
import java.io.IOException;
4+
import java.net.ProtocolException;
5+
import java.time.Duration;
6+
import java.util.LinkedHashMap;
7+
import java.util.Map;
8+
import java.util.function.Predicate;
9+
10+
import okhttp3.OkHttpClient;
11+
import okhttp3.Request;
12+
import okhttp3.Response;
13+
14+
public class RateLimitedClientWrapper {
15+
16+
private static final int REQUEST_RATE_LIMITED_WAIT_MS = 5_000;
17+
private static final Map<Predicate<String>, RateLimiter> FORCED_RATE_LIMITERS = Map.ofEntries(
18+
Map.entry(host -> host.endsWith("youtube.com"),
19+
RateLimiter.create(1.6, Duration.ofSeconds(1), 3.0))
20+
);
21+
22+
private final OkHttpClient client;
23+
private final Map<String, RateLimiter> hostRateLimiters = new LinkedHashMap<>();
24+
25+
public RateLimitedClientWrapper(final OkHttpClient client) {
26+
this.client = client;
27+
}
28+
29+
protected RateLimiter getRateLimiterFor(final Request request) {
30+
return hostRateLimiters.computeIfAbsent(request.url().host(), host ->
31+
FORCED_RATE_LIMITERS.entrySet()
32+
.stream()
33+
.filter(e -> e.getKey().test(host))
34+
.findFirst()
35+
.map(Map.Entry::getValue)
36+
.orElseGet(() ->
37+
// Default rate limiter per domain
38+
RateLimiter.create(5, Duration.ofSeconds(5), 3.0)));
39+
}
40+
41+
public Response executeRequestWithLimit(final Request request) throws IOException {
42+
Exception cause = null;
43+
for (int tries = 1; tries <= 3; tries++) {
44+
try {
45+
final double rateLimitedSec = getRateLimiterFor(request).acquire();
46+
System.out.println(
47+
"[RATE-LIMIT] Waited " + rateLimitedSec + "s for " + request.url());
48+
49+
final Response response = client.newCall(request).execute();
50+
if(response.code() != 429) { // 429 = Too many requests
51+
return response;
52+
}
53+
cause = new IllegalStateException("HTTP 429 - Too many requests");
54+
} catch (final ProtocolException pre) {
55+
if (!pre.getMessage().startsWith("Too many follow-up")) { // -> Too many requests
56+
throw pre;
57+
}
58+
cause = pre;
59+
}
60+
61+
final int waitMs = REQUEST_RATE_LIMITED_WAIT_MS * tries;
62+
System.out.println(
63+
"[TOO-MANY-REQUESTS] Waiting " + waitMs + "ms for " + request.url());
64+
try {
65+
Thread.sleep(waitMs);
66+
} catch (final InterruptedException iex) {
67+
Thread.currentThread().interrupt();
68+
}
69+
}
70+
throw new IllegalStateException("Retrying/Rate-limiting for " + request.url() + "failed", cause);
71+
}
72+
73+
public OkHttpClient getClient() {
74+
return client;
75+
}
76+
}

0 commit comments

Comments
 (0)