|
49 | 49 | import java.util.HashMap; |
50 | 50 | import java.util.List; |
51 | 51 | import java.util.Map; |
| 52 | +import java.util.Objects; |
52 | 53 | import java.util.Set; |
| 54 | +import java.util.concurrent.CompletableFuture; |
| 55 | +import java.util.concurrent.ExecutionException; |
| 56 | +import java.util.concurrent.Future; |
53 | 57 | import java.util.concurrent.TimeUnit; |
54 | 58 |
|
55 | 59 | import javax.xml.parsers.ParserConfigurationException; |
56 | 60 |
|
57 | 61 | 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 | + |
58 | 68 | private static final Cache<String, File> CACHE = CacheBuilder.newBuilder() |
59 | 69 | .expireAfterWrite(5, TimeUnit.MINUTES) |
60 | 70 | .build(); |
@@ -87,11 +97,45 @@ public static File manual(Project project, String artifact, boolean changing) { |
87 | 97 | return _download(project, artifact, changing, false, false, true); |
88 | 98 | } |
89 | 99 |
|
90 | | - |
91 | 100 | 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 | + |
93 | 135 | File ret = null; |
94 | 136 | try { |
| 137 | + Artifact art = Artifact.from(artifact); |
| 138 | + |
95 | 139 | ret = CACHE.getIfPresent(artifact); |
96 | 140 | if (ret != null && !ret.exists()) { |
97 | 141 | CACHE.invalidate(artifact); |
@@ -125,8 +169,15 @@ else if (repo instanceof GradleRepositoryAdapter) |
125 | 169 |
|
126 | 170 | if (ret != null) |
127 | 171 | CACHE.put(artifact, ret); |
| 172 | + |
| 173 | + future.complete(ret); |
128 | 174 | } catch (RuntimeException | IOException | URISyntaxException e) { |
| 175 | + future.completeExceptionally(e); |
129 | 176 | e.printStackTrace(); |
| 177 | + } finally { |
| 178 | + synchronized (ACTIVE_DOWNLOADS) { |
| 179 | + ACTIVE_DOWNLOADS.remove(downloadKey); |
| 180 | + } |
130 | 181 | } |
131 | 182 | return ret; |
132 | 183 | } |
@@ -290,4 +341,41 @@ private static File _downloadWithCache(Project project, URI maven, String path, |
290 | 341 | File target = Utils.getCache(project, "maven_downloader", path); |
291 | 342 | return Utils.downloadWithCache(url, target, changing, bypassLocal); |
292 | 343 | } |
| 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 | + |
293 | 381 | } |
0 commit comments