Fix ApacheTransport auth cache CCEx#1923
Conversation
As proposed, make session-wide auth cache key classloader aware. Fixes apache#1919
| private final int hashCode; | ||
|
|
||
| private Key(Object[] keys) { | ||
| this.keys = keys; |
There was a problem hiding this comment.
Minor defensive-programming note: the varargs array is stored directly without copying:
| this.keys = keys; | |
| private Key(Object[] keys) { | |
| this.keys = keys.clone(); | |
| this.hashCode = Arrays.hashCode(this.keys); | |
| } |
All current callers use varargs syntax (which creates a fresh array per call), so this is safe in practice today. But since Keys is a public API class in maven-resolver-api, a future caller could do:
Object[] arr = { "a", "b" };
Object key = Keys.of(arr);
arr[0] = "changed"; // silently corrupts the keyA clone() in the constructor would close that off. The cost is negligible since key arrays are tiny. Up to you whether you want to be defensive here — it's the kind of thing that burns someone exactly once, years from now.
| * long as they are used by components loaded once in system. As we saw, some subsystems like transports can be | ||
| * loaded multiple times. For example, in case of Maven, some transport may be present in Maven core, but also loaded up | ||
| * by some extension. In this case, key should distinguish between their {@link ClassLoader}s. | ||
| * <p/> |
There was a problem hiding this comment.
Very minor Javadoc nits:
-
<p/>(lines 31, 33, 36, 39) — self-closing<p/>is a no-op in HTML. Standard Javadoc convention is<p>(block-level paragraph separator). This appears to be specific to this new file — I didn't find<p/>elsewhere in themaven-resolver-apisources. -
Line 33: "keys our of object instances" → "keys out of object instances"
| requireNonNull(keys, "keys cannot be null"); | ||
| if (keys.length == 0) { | ||
| throw new IllegalArgumentException("keys must have at least one element"); | ||
| } else if (keys[0] instanceof Key) { |
There was a problem hiding this comment.
The subkey expansion only happens when a Key appears as the first element. If a Key appears at position > 0, it's treated as a regular element (compared by its own equals/hashCode). This is probably intentional for the "base key + suffix" use case, but worth mentioning in the Javadoc since it could surprise someone doing:
Object base = Keys.of("prefix");
Object k1 = Keys.of("other", base, "suffix"); // base is NOT expanded
Object k2 = Keys.of(base, "other", "suffix"); // base IS expanded → ["prefix", "other", "suffix"]The current Javadoc says "If the first instance of key elements is an object instance..." — I'd suggest making it slightly more explicit that expansion is first-position-only.
| return repositoryKeyFunctionFactory | ||
| .repositoryKeyFunction( | ||
| FileTrustedChecksumsSourceSupport.class, | ||
| RemoteRepositoryFilterSourceSupport.class, |
There was a problem hiding this comment.
Good catch. Confirmed via git blame: this was introduced in commit 79b6d5f2 (Repository Key Function SPI, #1679) where the repositoryKey() method was added. The FileTrustedChecksumsSourceSupport.class reference was clearly copy-pasted from that class's own repositoryKey() method and never updated to RemoteRepositoryFilterSourceSupport.class.
The practical effect was that RemoteRepositoryFilterSourceSupport subclasses and FileTrustedChecksumsSourceSupport shared the same repository key function cache entry in DefaultRepositoryKeyFunctionFactory. In practice this likely didn't cause visible bugs since both use the same default function, but it's semantically wrong and could mask issues if configuration diverged.
| }); | ||
| return (RemoteRepositoryFilter) session.getData() | ||
| .computeIfAbsent( | ||
| Keys.of(DefaultRemoteRepositoryFilterManager.class, "instance", session.hashCode()), () -> { |
There was a problem hiding this comment.
Pre-existing issue (not introduced by this PR), but worth noting: using `session.hashCode()` as a key element means that two different derived sessions with a hashCode collision would share the same filter instance. `Integer.hashCode()` is just the int value itself, so this relies on `RepositorySystemSession.hashCode()` being sufficiently unique. This was the same behavior before the PR (`INSTANCE_KEY + "." + session.hashCode()`), so no regression here.
| public final class OptionalDependencySelector implements DependencySelector { | ||
| public static final String IGNORED_KEYS = OptionalDependencySelector.class.getName() + ".ignored"; | ||
| public static final String UNSELECTED_KEYS = OptionalDependencySelector.class.getName() + ".unselected"; | ||
| public static final Object IGNORED_KEYS = Keys.of(OptionalDependencySelector.class, "ignored"); |
There was a problem hiding this comment.
Note: this changes two `public static final` fields from type `String` to type `Object`. While `OptionalDependencySelector` is in the `internal.impl` package (so the PR description's claim of "no API breakage in api, spi, util" is accurate), these fields are `public` and the class is `public final`.
If any external code (e.g., a custom `DependencySelector` implementation or a Maven plugin) references `OptionalDependencySelector.IGNORED_KEYS` and assigns it to a `String` variable, that code will fail to compile. Worth considering whether to keep the `String` type here (since these are only used as `SessionData` keys, and `SessionData.get()` accepts `Object`).
That said, the fields were only added in commit `0447a67c` (very recently), so the blast radius is likely near zero.
There was a problem hiding this comment.
Yes, these two keys are new, and are not yet present in any released code.
In-Depth Review: PR #1923 — Fix ApacheTransport auth cache CCExDesign AssessmentThe Placing The three key "flavors" (global/string-only, ClassLoader-aware/Class-element, private/Object-identity) emerge naturally from the element types without needing separate factory methods — this is elegant. Key Findings
Correctness AnalysisThe core fix is correct. The root cause of the ClassCastException is:
The fix uses All 15 migration sites use the right key scope:
API & CompatibilityThe PR description's claim of "no API breakage in public surface of api, spi, util modules" is verified as correct:
In
Test Coverage
Gaps:
Minor Notes
VerdictThe fix is correct and well-designed. The The findings above are all minor — the most actionable ones are the Javadoc typos and the optional defensive copy. The copy-paste bug fix in Overall: this looks good to merge, pending the minor fixes above. In-depth review by Claude Code on behalf of @gnodet |
Started as proposed, to make session-wide auth cache string key classloader aware. Then realized, we have "classloader aware" class, that we make into string, with tricks, to make it "classloader aware", and this sounds awkward. Hence,
Keyshelper class.This PR also revealed a bug, hidden deep, where (probably a copy pasted) code snippet used wrong key, see
RemoteRepositoryFilterSourceSupport.Back story: originally Resolver used
Stringas keys that as we know, are perfect candidates for keys. But, if we stick to Maven, same component may end up loaded twice in Maven, and if key value contains something that is component specific (like the issue mentioned Apache transport value is Apache transport specific class), this will cause inevitablyClassCastExif one instance puts something into map, and other one from the other classloader discovers it (and tries to cast it). In Maven, the resolver api, spi, util, impl and basic connector modules cannot be replaced (is prevented by Maven; circumvention is relocation or uber jar, changing the GAV), but other resolver modules can be loaded up multiple times, see https://github.com/apache/maven/blob/4106910f94e92a0967c61c62b4dbbcd6294c97a0/maven-core/src/main/resources/META-INF/maven/extension.xml#L185-L189Hence the
Keys, where user still has to decide how "scoped" the key should be, if usingStrings as key element, it will be global, and that is fine for keys mapped to values that are usable across class loaders as well (ie. value is consisted ofjava.baseclasses only). If we want class loader awareness (like in the issue, for Apache transport we do), then all user should do, is to use class loader specific class as one key element, and it will distinguish it from duplicate class loaded by another class loader. Finally, as update check manager does, "instance bound" keys are still possible too.This change, while may look "radical" or even breaking, is in fact limited and does not cause API breakage in public surface of modules: api, spi and util.
Fixes #1919