Skip to content

Fix #12445: Fix deadlock in AbstractRequestCache when resolving parent POMs#12446

Draft
gnodet wants to merge 5 commits into
apache:masterfrom
gnodet:fix/12445
Draft

Fix #12445: Fix deadlock in AbstractRequestCache when resolving parent POMs#12446
gnodet wants to merge 5 commits into
apache:masterfrom
gnodet:fix/12445

Conversation

@gnodet

@gnodet gnodet commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix deadlock in AbstractRequestCache.requests() that caused Maven 4 to hang indefinitely when building with an empty local repository (e.g., -Dmaven.repo.local=/tmp/fresh-repo)
  • Replace the Object.wait()/notify() pattern with direct value setting via CachingSupplier.complete(), eliminating the re-entrant deadlock scenario
  • Add tests for the re-entrant scenario and batch caching behavior

Root Cause

The requests() method used a wait/notify pattern with a local HashMap (nonCachedResults):

  1. An individualSupplier lambda was created that called nonCachedResults.wait(), expecting the batch resolution to populate results and call notifyAll()
  2. This individualSupplier was stored inside CachingSupplier instances via doCache(), which cached them by request key
  3. When a re-entrant requests() call occurred (e.g., parent POM resolution during artifact resolution), doCache() returned a CachingSupplier from the outer call — wrapping the outer call's individualSupplier
  4. The inner call's collection loop invoked this CachingSupplier, which waited on the outer call's nonCachedResults — a HashMap that could never be populated because the outer batch supplier was blocked waiting for the inner call to complete

Fix

  • Added CachingSupplier.complete(Object result) to directly set cached values without invoking the supplier
  • Changed requests() to pass a single-item fallback supplier to doCache() (instead of the wait-based individualSupplier), so any re-entrant retrieval can resolve independently
  • After batch resolution, results are set directly on CachingSupplier instances via complete()
  • Removed the synchronized wait/notify pattern on HashMap entirely

Test plan

  • Added testReentrantRequestsDoesNotDeadlock — reproduces the exact deadlock scenario (with 5-second timeout detection)
  • Added testBatchResultsAreCached — verifies batch results are properly cached for subsequent calls
  • All 499 existing tests in maven-impl pass
  • Full reactor build (mvn clean install -DskipTests) succeeds

🤖 Generated with Claude Code

gnodet and others added 4 commits July 8, 2026 15:20
… parent POMs

Replace the wait/notify pattern in AbstractRequestCache.requests() with
direct value setting via CachingSupplier.complete(). The old pattern
stored a wait-based individualSupplier in CachingSupplier instances
cached across calls. When a re-entrant requests() call (e.g., parent
POM resolution during artifact resolution) retrieved a CachingSupplier
from the cache, it would invoke the outer call's individualSupplier,
which waited on a HashMap that would never be notified — deadlock.

The fix:
- Add CachingSupplier.complete() to set values without invoking the supplier
- Pass a single-item fallback supplier to doCache() so newly created
  CachingSuppliers can independently resolve requests in re-entrant scenarios
- After batch resolution, directly set results via complete()
- Remove the synchronized wait/notify on HashMap entirely

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…otifyAll coordination

Add ThreadLocal re-entrancy detection and CachingSupplier.setBatchResolving()
so that concurrent threads wait for an in-progress batch result (via
Object.wait/notifyAll) instead of resolving the same request independently,
while same-thread re-entrant calls still use the fallback supplier to
avoid deadlock. Also fix CachingTestRequestCache to use ConcurrentHashMap,
clean up duplicate javadoc, and add concurrent resolution test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…le hashCode

ResolverRequest.hashCode() can change during batch resolution because
RequestTrace includes mutable ModelBuilderRequest data (identified in
PR apache#12166). Since reqToIndex always uses the same object references
for put and get, IdentityHashMap is safe and avoids missed lookups
that would cause redundant individual resolutions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verify that batch results are correctly delivered via complete() even
when request hashCode() changes during resolution (simulating
ResolverRequest with mutable RequestTrace/ModelBuilderRequest data).
The IdentityHashMap-based reqToIndex ensures lookups succeed by
reference identity regardless of hashCode mutations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a deadlock in AbstractRequestCache.requests() triggered by re-entrant resolution (e.g., parent POM resolution during artifact resolution), by replacing the previous wait/notify pattern with direct completion of cached suppliers.

Changes:

  • Introduces CachingSupplier.complete(Object) plus a batchResolving coordination flag to allow batch results to be published to cached suppliers and to let concurrent readers wait for completion.
  • Updates AbstractRequestCache.requests() to batch-resolve non-cached requests and populate CachingSupplier instances directly, with a ThreadLocal re-entrancy guard to avoid same-thread deadlocks.
  • Adds targeted tests covering re-entrant behavior, concurrent request/batch coordination, unstable hashCode() handling, and batch-result caching.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
impl/maven-impl/src/main/java/org/apache/maven/impl/cache/AbstractRequestCache.java Reworks batch request caching/coordination to avoid re-entrant deadlocks and publish results via complete().
impl/maven-impl/src/main/java/org/apache/maven/impl/cache/CachingSupplier.java Adds batch-completion API and waiting behavior for concurrent callers during in-progress batch resolution.
impl/maven-impl/src/test/java/org/apache/maven/impl/cache/AbstractRequestCacheTest.java Adds regression and concurrency tests for the deadlock scenario and caching behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +178 to +188
} catch (Throwable e) {
// Ensure waiting concurrent threads are unblocked on unexpected errors
CachingSupplier.AltRes failure = new CachingSupplier.AltRes(e);
for (REQ req : nonCached) {
nonCachedResults.put(
req, new CachingSupplier.AltRes(e.getCause())); // Mark as processed but failed
Integer idx = reqToIndex.get(req);
if (idx != null) {
suppliers.get(idx).complete(failure);
}
}
} finally {
nonCachedResults.notifyAll();
uncheckedThrow(e);
}
Comment on lines +106 to +108
void setBatchResolving(boolean resolving) {
this.batchResolving = resolving;
}
@gnodet gnodet added this to the 4.0.0-rc-6 milestone Jul 8, 2026
…and safe unblocking

- catch(Throwable) now falls through to the collection loop and produces
  a proper BatchRequestException with per-request results, consistent
  with the MavenExecutionException path.
- setBatchResolving(false) now calls notifyAll() to unblock any threads
  still waiting in apply() when complete() was never called (e.g., batch
  supplier returned fewer results than expected).
- Add test for partial batch result edge case.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants