Skip to content

Commit

Permalink
Implement retry-support and handling of 429
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte committed May 25, 2024
1 parent 935f8a6 commit 51af754
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public class DownloadAssetsCommand extends NeoFormEngineCommand {
@CommandLine.Option(names = "--asset-repository")
public URI assetRepository = URI.create("https://resources.download.minecraft.net/");

@CommandLine.Option(names = "--concurrent-downloads")
public int concurrentDownloads = 25;

/**
* Properties file that will receive the metadata of the asset index.
*/
Expand Down Expand Up @@ -118,8 +121,11 @@ protected void runWithNeoFormEngine(NeoFormEngine engine, List<AutoCloseable> cl
.filter(obj -> Files.notExists(objectsFolder.resolve(getObjectPath(obj))))
.toList();

// At most 50 concurrent downloads
var semaphore = new Semaphore(50);
if (concurrentDownloads < 1) {
throw new IllegalStateException("Cannot set concurrent downloads to less than 1: " + concurrentDownloads);
}

var semaphore = new Semaphore(concurrentDownloads);
try (var executor = Executors.newThreadPerTaskExecutor(DOWNLOAD_THREAD_FACTORY)) {
for (var object : objectsToDownload) {
var spec = new AssetDownloadSpec(object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -85,18 +86,31 @@ public boolean download(DownloadSpec spec, Path finalLocation, boolean silent) t
.header("User-Agent", USER_AGENT)
.build();

HttpResponse<Path> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(partialFile));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Download interrupted", e);
}
while (true) {
HttpResponse<Path> response;
try {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofFile(partialFile));
} catch (IOException e) {
// We do not have an API to get this information
if ("too many concurrent streams".equals(e.getMessage())) {
waitForRetry(1);
continue;
}
throw e;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Download interrupted", e);
}

if (response.statusCode() == 404) {
throw new FileNotFoundException(url.toString());
} else if (response.statusCode() != 200) {
throw new IOException("Failed to download " + url + ": " + response.statusCode());
if (response.statusCode() == 200) {
break;
} else if (response.statusCode() == 404) {
throw new FileNotFoundException(url.toString());
} else if (canRetryStatusCode(response.statusCode())) {
waitForRetry(response);
} else {
throw new IOException("Failed to download " + url + ": " + response.statusCode());
}
}

// Validate file
Expand Down Expand Up @@ -125,4 +139,33 @@ public boolean download(DownloadSpec spec, Path finalLocation, boolean silent) t
}
return true;
}

private static void waitForRetry(HttpResponse<?> response) throws IOException {
// We only support the version of this that specifies the delay in seconds
var retryAfter = response.headers().firstValueAsLong("Retry-After").orElse(5);
// Clamp some unreasonable delays to 5 minutes
waitForRetry(Math.clamp(retryAfter, 0, 300));
}

private static void waitForRetry(int seconds) throws IOException {
var waitUntil = Instant.now().plusSeconds(seconds);

while (Instant.now().isBefore(waitUntil)) {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for retry.", e);
}
}
}

private static boolean canRetryStatusCode(int statusCode) {
return statusCode == 408 // Request timeout
|| statusCode == 425 // Too early
|| statusCode == 429 // Rate-limit exceeded
|| statusCode == 502
|| statusCode == 503
|| statusCode == 504;
}
}

0 comments on commit 51af754

Please sign in to comment.