Skip to content

SOLR-18382: remove DocCollection.getReplicas(), migrate 60 call sites - #4760

Open
serhiy-bzhezytskyy wants to merge 17 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18382-remove-doccollection-getreplicas
Open

SOLR-18382: remove DocCollection.getReplicas(), migrate 60 call sites#4760
serhiy-bzhezytskyy wants to merge 17 commits into
apache:mainfrom
serhiy-bzhezytskyy:SOLR-18382-remove-doccollection-getreplicas

Conversation

@serhiy-bzhezytskyy

@serhiy-bzhezytskyy serhiy-bzhezytskyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

https://issues.apache.org/jira/browse/SOLR-18382

Removes DocCollection.getReplicas() (flattened every slice into a fresh ArrayList on each call) and migrates 60 call sites — the real count, not the 285 a plain .getReplicas() grep returns, since Slice.getReplicas() shares the name. The method was deleted first and the compiler's error list became the worklist.

Split by source set: 2 production sites, 3 in test-framework (a published artifact), 55 across 32 test files — that's why this ticket is large despite "low usage" being correct about production.

Where to look: CollectionTooManyReplicasTest.java has a local named slice already in scope at one call site, so the migrated loop there uses s instead — everywhere else uses slice. Three checks before trusting the swap: traversal order is unchanged (nothing was sorted), the three .toList() sites are read-only, and every asserted literal survives verbatim.

413 tests across 32 changed classes, 0 failures. Compile is the actual census here — a failing build's first report showed 2 sites; the true number surfaced only once test-framework compiled too.

SOLR-18378, SOLR-18380, SOLR-18381 and SOLR-18385 touch files this PR also touches — merging this one first should make those cleaner to extract.

AI-assisted (Claude Sonnet 5)

The method flattened every slice's replicas into a fresh ArrayList on each
call. DocCollection is already Iterable<Slice>, so callers iterate slices,
or use the idiom the tree already had in four places:

  X.getSlices().stream().flatMap(slice -> slice.getReplicas().stream())

The census had to come from the compiler, not from grep. `git grep
'\.getReplicas()'` returns 285 hits and only 60 are this method: the name
is declared seven times in the tree, and Slice.getReplicas() accounts for
most of the rest. No textual pattern can separate them, because the
receiver's name carries no type - the 60 real sites are reached through
eleven different variable names including `slices`, which is a
DocCollection named as if it were a Slice. So the method was deleted first
and the error list became the worklist.

And a failing build is not a complete census. The first compile reported
2 call sites; the true number was 60. It had died in test-framework, so
core's test compilation never ran. The list is the fixed point of
delete-compile-fix, not the first report.

Split by source set, because the two halves answer different questions:

  production src/java          2 sites  - the whole compatibility surface
  test-framework/src/java      3 sites  - a published artifact, so this
                                           counts as API
  src/test                    55 sites in 32 files

That makes the deprecation note - "low usage and builds an ArrayList
(surprising)" - correct about what it cares about. Two production callers
is low usage. It is the test migration that makes the ticket large.

Three things the type change could have broken, all checked. The removed
method returned List<Replica> while Slice.getReplicas() returns
Collection<Replica>, so sites needing indexed access collect to a real
list; the three .toList() sites are read-only, including the one passed
to assertDocsExistInAllReplicas, whose two overloads only iterate their
argument. Traversal order is unchanged, so sites doing .get(0) or
iterator().next() still pick the same replica; nothing was sorted. No
asserted value was altered - every expected literal (1, 2, 4, 8, 8, 9)
and every assertion message survives verbatim, and of 147 removed lines,
77 reappear identically modulo indentation from the added nesting.

One conversion needed care: turning a single loop into a nested pair
inside a lambda collided with an enclosing local named `slice` in
CollectionTooManyReplicasTest, which Java forbids; the loop variable is
`s` there.

Verified: compileJava and compileTestJava for the whole build,
spotlessCheck, ecjLintMain on core, solrj and test-framework, and all 32
changed test classes - 413 tests, 0 failures, 70 skipped, with every
class confirmed to have produced a result file rather than being
silently filtered out.

AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
Contributor Author

@dsmiley this one's yours (DocCollection.getReplicas(), deprecated Dec 2024) and it's the biggest migration in the batch -- 60 call sites across 37 files. Given the scale, a look from you before this merges would be genuinely useful, not just a courtesy tag.

AI-assisted (Claude Sonnet 5)

@dsmiley
dsmiley self-requested a review August 20, 2026 00:16

@dsmiley dsmiley left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks.
Admittedly it adds more code in many places. Maybe a getReplicaStream() method would be useful?

@dsmiley dsmiley added this to the 10.x milestone Aug 20, 2026
David Smiley's review on this PR: "Admittedly it adds more code in many
places. Maybe a getReplicaStream() method would be useful?" It would --
36 sites use the identical getSlices().stream().flatMap(slice ->
slice.getReplicas().stream()) two-liner, 8 more use
.mapToInt(s -> s.getReplicas().size()).sum() for a count. Both collapse
to one call each. Left the ~12 manual for-loops alone, some carry extra
logic inline.

AI-assisted (Claude Sonnet 5)
@serhiy-bzhezytskyy

Copy link
Copy Markdown
Contributor Author

Thanks! Added getReplicaStream() and refactored the 44 call sites that would benefit -- 36 identical .getSlices().stream().flatMap(slice -> slice.getReplicas().stream()) blocks and 8 .mapToInt(s -> s.getReplicas().size()).sum() counts collapse to one call each. Left the dozen manual for-loops alone since some carry extra logic inline.

25 test classes re-verified, including the two @nightly ones this touches (ShardSplitTest, TestPullReplica) -- 0 failures.

P.S. Three more sites use the same pattern outside this PR's diff -- CollectionHandlingUtils.java and CreateCollectionCmd.java (production code, not tests) plus CreateRoutedAliasTest.java -- left alone here since they're out of scope, but getReplicaStream() would help there too.

AI-assisted (Claude Sonnet 5)

@epugh epugh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bit more on hte fence on this one... I thought this would make the code easier to read, and i think it's harder... not sure the getReplicaStream is very useful... Especially when we add more .findFirst or .orThrows type clauses...

for (Replica r : coll.getReplicas()) {
if (replicaName.equals(r.getCoreName())) {
return r;
for (Slice slice : coll) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i am not quite getting this line? is coll a array and we are iterating over it? Or is col a single value, and we just map it to slice, so this for loop only fires actually 1 time?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

coll is a DocCollection, which is Iterable<Slice> -- a collection has one or more shards, so the outer loop runs once per shard, not once total. This is a direct unroll of what the old getReplicas() did internally (for (Slice slice : this) { replicas.addAll(slice.getReplicas()); }), just inlined instead of calling the now-removed method.

AI-assisted (Claude Sonnet 5)

c -> {
for (Replica r : c.getReplicas()) {
if (r.getState() != Replica.State.ACTIVE) return false;
for (Slice s : c) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

same not sure if we need this for loop?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as the ReindexCollectionCmd.java:718 thread -- c iterates its shards, one loop per shard.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up: simplified this one further to docCollection.getReplicaStream().allMatch(r -> r.getState() == Replica.State.ACTIVE) -- no extra logic here beyond the check, so it collapses cleanly.

AI-assisted (Claude Sonnet 5)

.getClusterState()
.getCollection(AbstractFullDistribZkTestBase.DEFAULT_COLLECTION);
Replica replica = defCol.getReplicas().get(0);
Replica replica = defCol.getReplicaStream().findFirst().orElseThrow();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suppose this is fine, but seems like there is some complex logic that maybe was hidden by teh get(0) that you now need to remember.. I think reading this that get(0) threw a exepction maybe if there are no replicas, but now with findFirst you have to add the .orElseThrow() to get the same behavior? I don't know if that is something that will trip people up? Or maybe the whole get(0) throwing an exception wasn't great to start with. Just a comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked both messages directly -- orElseThrow() throws NoSuchElementException: No value present, get(0) threw IndexOutOfBoundsException: Index 0 out of bounds for length 0. Different exception type, but neither is less clear than the other, so I don't think this trips anyone up. All 7 migrated getReplicaStream().findFirst() sites use .orElseThrow() the same way, none swallows an empty stream.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO this is beautiful/elegant and clear. @epugh , I take it you are not familiar (or is a hater of) the Java Stream api.

assertNotNull(docCollection);
// sanity check that everything is as before
assertEquals(9, docCollection.getReplicas().size());
assertEquals(9, docCollection.getReplicaStream().count());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not sure I see a big benefit in the getReplicaStream over getReplica.stream..

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think I was excpecting getReplicaStream to do more!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair -- for this specific line there's no real win, you're right, .getReplicaStream().count() isn't fancier than .getReplicas().size() was. The method's main job is replacing the verbose getSlices().stream().flatMap(slice -> slice.getReplicas().stream()) two-liner (73% of migrated call sites had that shape) -- this site just needed some replacement for the removed getReplicas(), and this is the plain flatten, not meant to do more.

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Streams sometimes "does more" behind the scenes, and that happened here. No internal wasted List accumulation since we don't need a list to count.

.getClusterState()
.getCollection(collectionName)
.getReplicaStream()
.skip(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We know we want to skip 1?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes -- .skip(1).findFirst() is the exact Stream equivalent of the old .get(1) (0-indexed, second element). The assumption that the added replica lands at index 1 isn't new here, it's inherited from the original .get(1) -- unchanged by this migration.

AI-assisted (Claude Sonnet 5)

new ReplicaData(replica.getShard(), coreName, (Long) version, numDocs);
log.info("{}", data);
results.put(coreName, data);
for (Slice slice : collectionState) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if using a more sonsistent variable name like collectionState instead of colls or c that I saw in other tests would have cued me better on this use of a for loop with Slice?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done as a separate commit -- renamed the 4 outlier coll/c/colState names to collectionState across the sites this PR touches. Also noticed the CollectionTooManyReplicasTest one you commented on had no extra logic beyond the check, so collapsed it to getReplicaStream().allMatch(...) instead, matching what the majority of sites already do.

AI-assisted (Claude Sonnet 5)

…or consistency

Eric Pugh noted a clearer, consistent name would have cued the Slice
loop's meaning better. Renamed the outlier `coll`/`c`/`colState` names
to `collectionState` at 4 sites this PR already touches; also collapsed
CollectionTooManyReplicasTest's plain "all replicas active" loop (no
extra logic beyond the check) to getReplicaStream().allMatch(), matching
the rule the other sites already followed.
Comment on lines 718 to 724
for (Slice slice : collectionState) {
for (Replica r : slice.getReplicas()) {
if (replicaName.equals(r.getCoreName())) {
return r;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Eric, if you want to see how Streams can sometimes "do more" in terms of reducing lines of code... well this thing right here screams for Streams. @serhiy-bzhezytskyy hint hint ;-)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done -- collapsed to getReplicaStream().filter(...).findFirst().orElse(null).

AI-assisted (Claude Sonnet 5)

Comment on lines -116 to -119
for (Replica r : c.getReplicas()) {
if (r.getState() != Replica.State.ACTIVE) return false;
}
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

could be a Stream one-liner

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already done -- see the earlier reply above, collapsed to getReplicaStream().allMatch(...) a few minutes before this comment was posted.

AI-assisted (Claude Sonnet 5)

Comment on lines 414 to 419
for (Slice slice : collectionState) {
for (Replica r : slice.getReplicas()) {
if (r.isActive(n) == false) return false;
}
}
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could be a Stream one-liner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done -- short-circuit && preserves the early return false on shard1 not active, then getReplicaStream().allMatch(...) replaces the double loop.

AI-assisted (Claude Sonnet 5)

Comment on lines 349 to 350
Collections.shuffle(replicas, random());
return replicas.get(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I find this painful. out of scope but trivial to change if you're in the moood

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done -- and made it more elegant per your own "no wasted List accumulation" point: getReplicaStream().collect(Collectors.toCollection(ArrayList::new)) builds the mutable list directly in one pass instead of toList() + a copying new ArrayList<>().

AI-assisted (Claude Sonnet 5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The painful aspect was hardly the list. It was randomizing the entire list and grabbing the first element. Why do that when we can pick a replica from the list randomly?

Comment on lines +75 to +78
for (Slice slice : clusterState.getCollection(DEFAULT_COLLECTION)) {
for (Replica replica : slice.getReplicas()) {
clients.add(getHttpSolrClient(replica));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stream forEach would be shorter

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done -- getReplicaStream().forEach(...).

AI-assisted (Claude Sonnet 5)

Comment on lines +376 to +380
.map(
c ->
c.getSlices().stream()
.mapToInt(s -> s.getReplicas().size())
.sum())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this is just the count of replicas. Elsewhere you had a far shorter Stream for this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done -- getReplicaStream().count().

AI-assisted (Claude Sonnet 5)

if (collectionState.getReplicas().size() == 1) {
Replica replica = collectionState.getReplicas().get(0);
List<Replica> replicas =
collectionState.getReplicaStream().collect(Collectors.toList());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Tell you're AI we're on Java 21 here and thus can call Stream.toList()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done -- Stream.toList() at both sites.

AI-assisted (Claude Sonnet 5)

if (collectionState.getReplicas().size() == 1) {
Replica replica = collectionState.getReplicas().get(0);
List<Replica> replicas =
collectionState.getReplicaStream().collect(Collectors.toList());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

again

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same fix.

AI-assisted (Claude Sonnet 5)

Comment on lines +82 to +87
for (Slice slice : clusterState.getCollection(COLL)) {
for (Replica replica : slice.getReplicas()) {
clients.add(
new CollectionScopedSolrClient(
cluster.getReplicaJetty(replica).getSolrClient(), replica.getCoreName()));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider Stream to avoid double-loop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done -- same fix.

AI-assisted (Claude Sonnet 5)

Comment thread solr/solrj/src/java/org/apache/solr/common/cloud/DocCollection.java Outdated
"...without allocating an intermediate list" named the removed method's
behavior by negation instead of just stating the fact.
…one-liner

Pure find-first-match with no extra logic, same shape as the other
migrated sites -- collapses to filter().findFirst().orElse(null).
Short-circuit && preserves the early return-false on shard1 not
being active, then getReplicaStream().allMatch() replaces the
manual double loop.
Still needs a mutable List for Collections.shuffle(), so wrapped in a
new ArrayList<> rather than returning the stream's own immutable list.
}
return replicas;
/** Stream of all replicas across all slices. */
public Stream<Replica> getReplicaStream() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we don't merely get something we have (we don't already have a Stream), maybe the name of this method should be replicaStream ? OTOH, nobody ought to ever think a Stream returning method is saved/persisted; they are always ephemeral. Hoping to here from @serhiy-bzhezytskyy and not your AI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

:) i think it is obvious that it smells now ...
look at this one: getReplicaStream().filter(...).findFirst().orElse(null).
it doesn't look fluently (aka Fluent API), right? so i think it should be replicaStream().filter(...).findFirst().orElse(null). etc
@dsmiley if it does make sense for you/us then i will be happy (not AI term) to make such refactoring not only here but we i will feel the same smell

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cool let's do the rename. To be clear, I meant I wanted your opinion, not an AI's opinion specifically here.

…e allocation

toList() + new ArrayList<>(...) built two lists; Collectors.toCollection
builds the mutable one directly in a single pass.
…view

Pure per-replica assertion with no accumulation -- getReplicaStream()
+ forEach() replaces the double loop.
Solr is on Java 21; both sites only read the list (size()/get(0)),
so the immutable list from toList() is a safe drop-in.
Comment thread solr/core/src/test/org/apache/solr/cloud/MoveReplicaTest.java Outdated
David: we aren't returning something we already have, so a getter-style
prefix is wrong for a Stream-returning method -- matches the JDK's own
convention (Collection.stream(), not Collection.getStream()).
David: we don't need to manipulate the list merely to pick one at
random -- toList() (immutable) + a random index avoids the shuffle
and the mutable-list requirement entirely.
@@ -0,0 +1,8 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
title: Remove the deprecated DocCollection.getReplicas() method, which allocated a new list on every call. DocCollection is Iterable<Slice>, so iterate its slices and call Slice.getReplicas(), or use getSlices().stream().flatMap(slice -> slice.getReplicas().stream()).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's be brief on internal matters where even a changelog is debatable.

Suggested change
title: Remove the deprecated DocCollection.getReplicas() method, which allocated a new list on every call. DocCollection is Iterable<Slice>, so iterate its slices and call Slice.getReplicas(), or use getSlices().stream().flatMap(slice -> slice.getReplicas().stream()).
title: Remove DocCollection.getReplicas() and add similar replicaStream()

As I read this... gosh, we're wasting time with such a changelog. That's the thing -- ask yourself, will a user of Solr care? Honestly, no.

.map(Replica::getNodeName)
.distinct()
.collect(Collectors.toList());
return state.replicaStream().map(Replica::getNodeName).distinct().collect(Collectors.toList());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
return state.replicaStream().map(Replica::getNodeName).distinct().collect(Collectors.toList());
return state.replicaStream().map(Replica::getNodeName).distinct().toList();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants