Skip to content

Commit 19fca01

Browse files
committed
Limit parallel download requests
1 parent 9c178d0 commit 19fca01

File tree

2 files changed

+109
-11
lines changed

2 files changed

+109
-11
lines changed

src/main/java/org/spongepowered/gradle/vanilla/resolver/jdk/JdkHttpClientDownloader.java

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ public class JdkHttpClientDownloader implements Downloader {
6161
private final ResolveMode resolveMode;
6262
private final boolean writeToDisk;
6363

64+
/**
65+
* We limit the number of parallel requests otherwise:
66+
* - The JDK throws an error when we reach HTTP/2 max_concurrent_streams (usually around 100).
67+
* - Mojang servers randomly return error pages (200 HTTP code with HTML message "The request is blocked.").
68+
* The maximum of 8 parallel requests has been determined purely empirically by trying several powers of two.
69+
*/
70+
private final TaskQueue queue = new TaskQueue(8); // TODO configurable max, per host?
71+
6472
/**
6573
* Create a downloader that does not cache.
6674
*
@@ -235,21 +243,23 @@ private <T> CompletableFuture<ResolutionResult<T>> sendRequest(final URI uri, fi
235243
if (etag != null) {
236244
requestBuilder.header(HttpConstants.HEADER_IF_NONE_MATCH, etag);
237245
}
238-
return this.client.sendAsync(requestBuilder.build(), bodyHandler).thenApply(message -> {
239-
switch (message.statusCode()) {
240-
case HttpConstants.STATUS_NOT_FOUND:
241-
return ResolutionResult.notFound();
242-
case HttpConstants.STATUS_OK:
243-
return ResolutionResult.result(message.body(), false); // Known invalid, hash does not match expected.
244-
default:
245-
throw new CompletionException(new HttpErrorResponseException(uri, message.statusCode(), message.toString()));
246-
}
247-
});
246+
final HttpRequest request = requestBuilder.build();
247+
return this.queue.run(() -> this.client.sendAsync(request, bodyHandler))
248+
.thenApply(message -> {
249+
switch (message.statusCode()) {
250+
case HttpConstants.STATUS_NOT_FOUND:
251+
return ResolutionResult.notFound();
252+
case HttpConstants.STATUS_OK:
253+
return ResolutionResult.result(message.body(), false); // Known invalid, hash does not match expected.
254+
default:
255+
throw new CompletionException(new HttpErrorResponseException(uri, message.statusCode(), message.toString()));
256+
}
257+
});
248258
}
249259

250260
@Override
251261
public void close() throws IOException {
252-
// nothing needed, the client just relies on the executor
262+
this.queue.close(); // abort pending requests
253263
}
254264

255265
// body subscribers
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* This file is part of VanillaGradle, licensed under the MIT License (MIT).
3+
*
4+
* Copyright (c) SpongePowered <https://www.spongepowered.org>
5+
* Copyright (c) contributors
6+
*
7+
* Permission is hereby granted, free of charge, to any person obtaining a copy
8+
* of this software and associated documentation files (the "Software"), to deal
9+
* in the Software without restriction, including without limitation the rights
10+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
* copies of the Software, and to permit persons to whom the Software is
12+
* furnished to do so, subject to the following conditions:
13+
*
14+
* The above copyright notice and this permission notice shall be included in
15+
* all copies or substantial portions of the Software.
16+
*
17+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
* THE SOFTWARE.
24+
*/
25+
package org.spongepowered.gradle.vanilla.resolver.jdk;
26+
27+
import java.util.LinkedList;
28+
import java.util.Queue;
29+
import java.util.concurrent.CompletableFuture;
30+
import java.util.function.Supplier;
31+
32+
class TaskQueue implements AutoCloseable {
33+
private final int max;
34+
private boolean closed;
35+
private int running;
36+
private final Queue<CompletableFuture<Void>> pending = new LinkedList<>();
37+
38+
public TaskQueue(final int max) {
39+
if (max < 1) {
40+
throw new IllegalArgumentException("max must be >= 1");
41+
}
42+
this.max = max;
43+
}
44+
45+
private synchronized CompletableFuture<Void> acquire() {
46+
if (this.closed) {
47+
return CompletableFuture.failedFuture(new IllegalStateException("queue closed"));
48+
}
49+
if (this.running < this.max) {
50+
this.running++;
51+
return CompletableFuture.completedFuture(null);
52+
}
53+
final CompletableFuture<Void> future = new CompletableFuture<>();
54+
this.pending.add(future);
55+
return future;
56+
}
57+
58+
private synchronized void release() {
59+
if (this.running > this.max) {
60+
this.running--;
61+
return;
62+
}
63+
final CompletableFuture<Void> next = this.pending.poll();
64+
if (next == null) {
65+
this.running--;
66+
} else {
67+
next.complete(null);
68+
}
69+
}
70+
71+
public <T> CompletableFuture<T> run(final Supplier<CompletableFuture<T>> task) {
72+
return this.acquire().thenCompose(_ -> task.get()).whenComplete((_, _) -> this.release());
73+
}
74+
75+
@Override
76+
public synchronized void close() {
77+
if (!this.closed) {
78+
this.closed = true;
79+
if (!this.pending.isEmpty()) {
80+
final Exception ex = new IllegalStateException("queue closed");
81+
for (final CompletableFuture<Void> future : this.pending) {
82+
future.completeExceptionally(ex);
83+
}
84+
this.pending.clear();
85+
}
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)