Skip to content

Commit 0c2ea04

Browse files
authored
Fix issues when artifacts are being downloaded concurrently (#755)
1 parent 635a1f4 commit 0c2ea04

File tree

1 file changed

+90
-2
lines changed

1 file changed

+90
-2
lines changed

src/common/java/net/minecraftforge/gradle/common/util/MavenArtifactDownloader.java

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,22 @@
4949
import java.util.HashMap;
5050
import java.util.List;
5151
import java.util.Map;
52+
import java.util.Objects;
5253
import java.util.Set;
54+
import java.util.concurrent.CompletableFuture;
55+
import java.util.concurrent.ExecutionException;
56+
import java.util.concurrent.Future;
5357
import java.util.concurrent.TimeUnit;
5458

5559
import javax.xml.parsers.ParserConfigurationException;
5660

5761
public class MavenArtifactDownloader {
62+
/**
63+
* This tracks downloads that are <b>currently</b> active. As soon as a download has finished it will be removed
64+
* from this map.
65+
*/
66+
private static final Map<DownloadKey, Future<File>> ACTIVE_DOWNLOADS = new HashMap<>();
67+
5868
private static final Cache<String, File> CACHE = CacheBuilder.newBuilder()
5969
.expireAfterWrite(5, TimeUnit.MINUTES)
6070
.build();
@@ -87,11 +97,45 @@ public static File manual(Project project, String artifact, boolean changing) {
8797
return _download(project, artifact, changing, false, false, true);
8898
}
8999

90-
91100
private static File _download(Project project, String artifact, boolean changing, boolean generated, boolean gradle, boolean manual) {
92-
Artifact art = Artifact.from(artifact);
101+
/*
102+
* This somewhat convoluted code is necessary to avoid race-conditions when two Gradle worker threads simultaneously
103+
* try to download the same artifact.
104+
* The first thread registers a future that other threads can wait on.
105+
* Once it finishes, the future will be removed and subsequent calls will use the CACHE instead.
106+
* We use all parameters of the function as the key here to prevent subtle bugs where the same artifact
107+
* is looked up simultaneously with different resolver-options, leading only to one attempt being made.
108+
*/
109+
DownloadKey downloadKey = new DownloadKey(project, artifact, changing, generated, gradle, manual);
110+
CompletableFuture<File> future;
111+
synchronized (ACTIVE_DOWNLOADS) {
112+
Future<File> activeDownload = ACTIVE_DOWNLOADS.get(downloadKey);
113+
if (activeDownload != null) {
114+
// Some other thread is already working downloading this exact artifact, wait for it to finish
115+
try {
116+
project.getLogger().info("Waiting for download of {} on other thread", artifact);
117+
return activeDownload.get();
118+
} catch (InterruptedException e) {
119+
throw new RuntimeException(e);
120+
} catch (ExecutionException e) {
121+
if (e.getCause() instanceof RuntimeException) {
122+
throw (RuntimeException) e.getCause();
123+
} else {
124+
throw new RuntimeException(e.getCause());
125+
}
126+
}
127+
} else {
128+
project.getLogger().info("Downloading {}", artifact);
129+
// We're the first thread to download the artifact, make sure concurrent downloads just wait for us
130+
future = new CompletableFuture<>();
131+
ACTIVE_DOWNLOADS.put(downloadKey, future);
132+
}
133+
}
134+
93135
File ret = null;
94136
try {
137+
Artifact art = Artifact.from(artifact);
138+
95139
ret = CACHE.getIfPresent(artifact);
96140
if (ret != null && !ret.exists()) {
97141
CACHE.invalidate(artifact);
@@ -125,8 +169,15 @@ else if (repo instanceof GradleRepositoryAdapter)
125169

126170
if (ret != null)
127171
CACHE.put(artifact, ret);
172+
173+
future.complete(ret);
128174
} catch (RuntimeException | IOException | URISyntaxException e) {
175+
future.completeExceptionally(e);
129176
e.printStackTrace();
177+
} finally {
178+
synchronized (ACTIVE_DOWNLOADS) {
179+
ACTIVE_DOWNLOADS.remove(downloadKey);
180+
}
130181
}
131182
return ret;
132183
}
@@ -290,4 +341,41 @@ private static File _downloadWithCache(Project project, URI maven, String path,
290341
File target = Utils.getCache(project, "maven_downloader", path);
291342
return Utils.downloadWithCache(url, target, changing, bypassLocal);
292343
}
344+
345+
/**
346+
* Key used to track active downloads and avoid downloading the same file in two threads concurrently,
347+
* leading to corrupted files on disk.
348+
*/
349+
private static class DownloadKey {
350+
private final Project project;
351+
private final String artifact;
352+
private final boolean changing;
353+
private final boolean generated;
354+
private final boolean gradle;
355+
private final boolean manual;
356+
357+
DownloadKey(Project project, String artifact, boolean changing, boolean generated, boolean gradle, boolean manual) {
358+
this.project = project;
359+
this.artifact = artifact;
360+
this.changing = changing;
361+
this.generated = generated;
362+
this.gradle = gradle;
363+
this.manual = manual;
364+
}
365+
366+
@Override
367+
public boolean equals(Object o) {
368+
if (this == o) return true;
369+
if (o == null || getClass() != o.getClass()) return false;
370+
DownloadKey that = (DownloadKey) o;
371+
return changing == that.changing && generated == that.generated && gradle == that.gradle && manual == that.manual && project.equals(that.project) && artifact.equals(that.artifact);
372+
}
373+
374+
@Override
375+
public int hashCode() {
376+
return Objects.hash(project, artifact, changing, generated, gradle, manual);
377+
}
378+
379+
}
380+
293381
}

0 commit comments

Comments
 (0)