Fix for ChainedWorkspaceReader#1909
Conversation
The readers are immutable, and repository is derived from (immutable) readers, hence, repository is immutable as well.
gnodet
left a comment
There was a problem hiding this comment.
Hi @cstamas, thanks for the clean-up! The reasoning is sound — since WorkspaceRepository is immutable and Key captures repository keys at construction time, the lazy-recreation logic in getRepository() is indeed redundant given that the readers list is now fixed at construction time.
A few minor points worth discussing before merging:
-
Null-handling behaviour change (see inline): passing a literal
nullvarargs now throwsNPEimmediately (previously it was silently treated as an empty list), while individualnullelements in the array are now silently dropped (previously they were included and would NPE later). Neither case is part of the documented contract, but it’s a subtle behaviour change that could surprise callers. -
Undocumented assumption about reader stability (see inline): the correctness of the new code rests on the assumption that
WorkspaceReaderimplementations return a stableWorkspaceRepository(same key) for their entire lifetime.WorkspaceRepositoryis immutable, which makes this very likely in practice, butWorkspaceReaderis an interface. A brief comment capturing this assumption would make the code easier to reason about for future contributors. -
Minor style (see inline):
Collectors.toCollection(ArrayList::new)can be simplified. -
Tests: there are no unit tests for
ChainedWorkspaceReader. A basic test covering construction,findArtifact,findVersions, andgetRepositorywould be a nice addition — especially one that verifies the null-filtering behaviour now that it is an explicit choice.
The @Override additions are good hygiene — thanks for those.
Overall this is a reasonable simplification; just wanted to flag the subtle behaviour differences. Happy to see it land once the above is addressed (or consciously accepted).
— Guillaume (gnodet)
| } | ||
|
|
||
| ArrayList<WorkspaceReader> list = | ||
| Arrays.stream(readers).filter(Objects::nonNull).collect(Collectors.toCollection(ArrayList::new)); |
There was a problem hiding this comment.
Nit: note that this slightly changes the null-handling behaviour compared to the previous code. The old Collections.addAll(this.readers, readers) would silently accept a null varargs array (treating it as empty), whereas Arrays.stream(readers) will throw NullPointerException if readers itself is null. On the other hand, individual null elements in the array were previously included (causing an NPE later when iterating), while now they are filtered out. Worth documenting these behaviour changes in Javadoc or a comment, even if null inputs aren’t part of the intended public contract.
| if (!key.equals(repository.getKey())) { | ||
| repository = new WorkspaceRepository(repository.getContentType(), key); | ||
| } | ||
| return repository; |
There was a problem hiding this comment.
The simplification here hinges on the assumption that WorkspaceReader.getRepository() always returns a repository whose getKey() is stable for the lifetime of the reader. WorkspaceRepository itself is indeed immutable (its key field is final), but WorkspaceReader is an interface — an implementation could theoretically return a different WorkspaceRepository instance with a different key on each invocation. The old code defended against that case by re-checking the key on every call. Would be good to document this assumption, e.g. with a comment like // Assumes WorkspaceReader.getRepository() returns a stable (immutable) repository for the reader’s lifetime.
| ArrayList<WorkspaceReader> list = | ||
| Arrays.stream(readers).filter(Objects::nonNull).collect(Collectors.toCollection(ArrayList::new)); |
There was a problem hiding this comment.
Minor style point: since the collected list is immediately wrapped in Collections.unmodifiableList, the exact ArrayList implementation doesn’t matter. You could simplify to .collect(Collectors.toList()), or, since maven-resolver targets Java 11+, even use Stream.of(readers).filter(Objects::nonNull).collect(Collectors.toUnmodifiableList()) (Java 10+).
|
@gnodet Windows failed with See https://github.com/apache/maven-resolver/actions/runs/27139141318/job/80106163823?pr=1909 |
|
The Root cause: When the loser thread's lock acquisition times out in A secondary issue: |
|
Fixed method |
gnodet
left a comment
There was a problem hiding this comment.
Thanks @cstamas — the previous concerns are well addressed:
- Null handling: the null-filtering behaviour is now explicit and well-tested (
constructNullReader,constructNullVararg,constructMultipleWithNulls). Good. - Reader stability / unstable keys: understood and agreed — IDE integrations do use unstable keys. The
AtomicReference+getAndUpdateapproach ingetRepository()handles this correctly and thread-safely. The comment fromWorkspaceRepository(String, Object)constructor already documents this expectation. - Tests: comprehensive coverage including concurrency — nice work.
Two minor observations (non-blocking):
-
concurrentKeyChangetest:ArrayListis not thread-safe — the concurrentresults.add()from multiple threads could corrupt the list. ConsiderCollections.synchronizedList(new ArrayList<>(threads))or aConcurrentLinkedQueue. Unlikely to cause issues in practice with only 10 threads, but it's technically racy. -
getAndUpdate+get()ingetRepository():getAndUpdatereturns the old value, thenget()fetches the current one — which another thread may have changed in between. UsingupdateAndGetinstead would return the value this call produced. Given unstable keys, this is inconsequential in practice (any returned value is potentially stale by the time the caller uses it), butupdateAndGetwould be slightly more precise.
Stream style (Arrays.stream().filter().forEach() vs collectors) — no change needed, it's readable as-is.
LGTM overall. Approving.
Claude Code on behalf of Guillaume Nodet
The reader instances once set, are immutable, but repository instances returned by readers may change (their key).