Skip to content

Commit 89de948

Browse files
Flossyclaude
andcommitted
docs: improve javadoc for ensureJarCached locking strategy
Clarify FutureTask-based per-artifact locking implementation. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 6770794 commit 89de948

1 file changed

Lines changed: 45 additions & 11 deletions

File tree

src/main/java/org/flossware/classloader/MavenNexusClassSource.java

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import org.flossware.classloader.util.ClassNameUtil;
44

55
import java.io.ByteArrayOutputStream;
6-
76
import static org.flossware.classloader.util.ClassLoaderConstants.DEFAULT_BUFFER_SIZE;
87
import java.io.IOException;
98
import java.io.InputStream;
@@ -188,17 +187,22 @@ private String buildJarUrl(MavenArtifact artifact) {
188187
/**
189188
* Ensures a JAR file is cached for the given artifact.
190189
* Downloads the JAR once and reuses it for subsequent class extractions.
191-
* Uses per-artifact locking to allow concurrent downloads of different artifacts.
190+
*
191+
* <p>Uses a FutureTask-based per-artifact locking pattern so that downloads of different
192+
* artifacts proceed fully in parallel (no shared lock or ConcurrentHashMap bin-lock held
193+
* during I/O), while duplicate downloads of the same artifact are coalesced -- only the
194+
* first thread downloads, and subsequent threads wait on the same Future.</p>
192195
*
193196
* @param artifactKey The artifact identifier
194197
* @param jarUrl The URL to download the JAR from
195198
* @return A JarFile instance opened on the cached JAR
196199
* @throws IOException if download or JAR opening fails
197200
*/
198201
private JarFile ensureJarCached(String artifactKey, String jarUrl) throws IOException {
199-
return jarFileCache.computeIfAbsent(artifactKey, key -> {
200-
try {
201-
// Download JAR to temp file
202+
// Create a FutureTask that encapsulates the download work
203+
FutureTask<JarFile> newTask = new FutureTask<>(new Callable<JarFile>() {
204+
@Override
205+
public JarFile call() throws IOException {
202206
Path tempJarPath = Files.createTempFile("jclassloader-nexus-", ".jar");
203207
try {
204208
downloadJarFile(jarUrl, tempJarPath);
@@ -214,12 +218,35 @@ private JarFile ensureJarCached(String artifactKey, String jarUrl) throws IOExce
214218
}
215219
throw e;
216220
}
217-
} catch (IOException e) {
218-
// Wrap IOException in RuntimeException for computeIfAbsent compatibility
219-
// This will be caught and re-thrown as IOException by the caller
220-
throw new UncheckedIOException(e);
221221
}
222222
});
223+
224+
// Atomically insert the task -- only the winner actually runs the download.
225+
// putIfAbsent returns null if this thread won, or the existing task otherwise.
226+
FutureTask<JarFile> existingTask = jarFileCache.putIfAbsent(artifactKey, newTask);
227+
if (existingTask == null) {
228+
// This thread won the race -- run the download (outside any CHM lock)
229+
newTask.run();
230+
existingTask = newTask;
231+
}
232+
233+
try {
234+
return existingTask.get();
235+
} catch (ExecutionException e) {
236+
// Download failed -- remove the poisoned future so retries can try again
237+
jarFileCache.remove(artifactKey, existingTask);
238+
Throwable cause = e.getCause();
239+
if (cause instanceof IOException) {
240+
throw (IOException) cause;
241+
}
242+
throw new IOException("Failed to download artifact: " + artifactKey, cause);
243+
} catch (InterruptedException e) {
244+
Thread.currentThread().interrupt();
245+
throw new IOException("Interrupted while waiting for artifact download: " + artifactKey, e);
246+
} catch (CancellationException e) {
247+
jarFileCache.remove(artifactKey, existingTask);
248+
throw new IOException("Artifact download was cancelled: " + artifactKey, e);
249+
}
223250
}
224251

225252
private void downloadJarFile(String jarUrl, Path tempJarPath) throws IOException {
@@ -412,9 +439,16 @@ public void close() throws IOException {
412439
List<IOException> exceptions = new ArrayList<>();
413440

414441
// Close all cached JAR files
415-
for (JarFile jarFile : jarFileCache.values()) {
442+
for (FutureTask<JarFile> task : jarFileCache.values()) {
416443
try {
417-
jarFile.close();
444+
if (task.isDone() && !task.isCancelled()) {
445+
task.get().close();
446+
}
447+
} catch (ExecutionException e) {
448+
// Task failed during download -- nothing to close
449+
} catch (InterruptedException e) {
450+
Thread.currentThread().interrupt();
451+
exceptions.add(new IOException("Interrupted during close", e));
418452
} catch (IOException e) {
419453
exceptions.add(e);
420454
}

0 commit comments

Comments
 (0)