Fix deadlocks in request cache (unstable hashCode, re-entrant self-deadlock, mutable properties)#12166
Open
gnodet wants to merge 3 commits into
Open
Fix deadlocks in request cache (unstable hashCode, re-entrant self-deadlock, mutable properties)#12166gnodet wants to merge 3 commits into
gnodet wants to merge 3 commits into
Conversation
The `requests()` method uses a HashMap to coordinate batch results between the batch supplier and the individual CachingSupplier instances. When a request object's hashCode() changes between HashMap.put() and HashMap.containsKey() (e.g., because ResolverRequest includes a RequestTrace with mutable ModelBuilderRequest data), the key is stored in one bucket but looked up in another, causing containsKey() to return false. The individualSupplier then waits forever for a result that is already in the map but unreachable. Switch to IdentityHashMap which uses System.identityHashCode() and reference equality (==), both of which are stable regardless of mutable object state. This is safe because the code always uses the same object reference for put and get. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mputation When requests() stores CachingSuppliers wrapping individualSupplier in the session cache, a re-entrant call from the same thread can self-deadlock: the batch computation triggers request() which finds the cached entry, calls individualSupplier.apply() which wait()s for the batch to complete, but the batch can't complete because this thread is now blocked. Track the computing thread in CachingSupplier and throw CyclicCacheAccessException on re-entrant access. The request() method catches this and falls back to computing directly with the caller's supplier, bypassing the cache for that one call. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gnodet
added a commit
that referenced
this pull request
May 28, 2026
Cherry-pick from fix/identity-hashmap-request-cache branch. When requests() stores CachingSuppliers wrapping individualSupplier in the session cache, a re-entrant call from the same thread self-deadlocks. Track the computing thread and fall back to direct computation on re-entrant access. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When systemProperties or userProperties are null, the constructor fell through to session.getSystemProperties() without Map.copyOf(). That returns a live view over java.util.Properties, whose hashCode() mutates if System.setProperty() is called. Since ModelBuilderRequest ends up as RequestTrace.data in parent traces, and ResolverRequest.hashCode() recurses through the entire trace chain, this made cache keys unstable. Always wrap in Map.copyOf() regardless of the source. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gnodet
added a commit
that referenced
this pull request
May 29, 2026
) Cherry-pick from fix/identity-hashmap-request-cache branch. DefaultModelBuilderRequest stored a live view of session properties backed by java.util.Properties when systemProperties was null. Now always wraps in Map.copyOf() to ensure immutable snapshot. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes two deadlock vectors and one root-cause mutability bug in the Maven 4 request cache layer, responsible for ~80 timeout failures in Maven 4 compatibility testing (projects using
maven-remote-resources-pluginhang indefinitely).Root Cause Analysis
The hang occurs during parent POM resolution triggered by the remote-resources-plugin's lazy
getProjects()call, which enters the compat resolver →projectBuilder.build()→ Maven 4ArtifactResolver→ request cache. Two independent deadlock vectors exist:1. Unstable
hashCode()innonCachedResultsHashMapAbstractRequestCache.requests()uses a localHashMapto coordinate batch results. The batch supplier puts results viaput(), and theindividualSupplierlambda retrieves them viacontainsKey().ResolverRequestis a Java record whosehashCode()transitively includesRequestTrace.data→ModelBuilderRequest→systemProperties. WhensystemPropertiesis null in the builder,DefaultModelBuilderRequeststores a live view ofsession.getSystemProperties()(backed byjava.util.Properties) instead of a snapshot. IfSystem.setProperty()is called betweenput()andcontainsKey(), the hash changes, the entry is stored in one bucket but looked up in another, andcontainsKey()returnsfalse— the thread waits forever.2. Re-entrant self-deadlock via
CachingSupplierWhen
requests()storesCachingSupplierinstances wrappingindividualSupplierin the shared session cache, a re-entrant call from the same thread during batch computation can self-deadlock:requests([req])→doCache(req, individualSupplier)createsCachingSupplierCS in cachesynchronized(nonCachedResults)request(req)→ finds CS in cachecs.apply(req)→synchronized(cs)is re-entrant (same thread) →valueis null → callsindividualSupplierindividualSupplier→synchronized(nonCachedResults)(re-entrant) →containsKey(req)→ false (batch not done) →wait()→ releasesnonCachedResultsmonitorwait(), but it is also the thread running the batch —notifyAll()only fires after the batch returns, which can never happenFix (3 commits)
Commit 1:
IdentityHashMapfornonCachedResultsSwitch from
HashMaptoIdentityHashMapinAbstractRequestCache.requests(). Since the code always uses the same object references forput()andcontainsKey()within a singlerequests()call, identity-based lookup is correct and immune to anyhashCode()instability. This is a defense-in-depth measure:RequestTrace.datais typed asObjectwith no immutability guarantee, so any mutable data in the trace chain could trigger the same issue.Commit 2: Re-entrant self-deadlock detection in
CachingSupplierTrack the computing thread in
CachingSupplier. Whenapply()detects re-entrant access from the same thread (the thread that is already computing the value), it throwsCyclicCacheAccessException.AbstractRequestCache.request()catches this and falls back to computing directly with the caller's original supplier, bypassing the cache for that one call. The outer batch computation continues normally and sets the cached value for future callers.Commit 3: Snapshot system/user properties in
ModelBuilderRequestDefaultModelBuilderRequestconstructor usedsession.getSystemProperties()directly whensystemPropertieswas null, storing a live view backed byjava.util.Properties. Now always wraps inMap.copyOf()regardless of the source, ensuring the stored map is a true immutable snapshot. Same fix applied touserProperties.Test plan
httpcomponents-stylecheckhangs indefinitely with Maven 4mvn verify -DskipTests) passesAbstractRequestCacheTestpassesClaude Code on behalf of Guillaume Nodet