-
Notifications
You must be signed in to change notification settings - Fork 674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SOLR-17582 Stream CLUSTERSTATUS API response #2916
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for contributing!
if (shard != null) { | ||
String[] paramShards = shard.split(","); | ||
requestedShards.addAll(Arrays.asList(paramShards)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally this is done up front; not per iteration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved it out so its not per iteration.
byte[] bytes = Utils.toJSON(clusterStateCollection); | ||
@SuppressWarnings("unchecked") | ||
Map<String, Object> docCollection = (Map<String, Object>) Utils.fromJSON(bytes); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why round-trip this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So looked at this and I understand now why it was done this way. It wants to just write this out to the TextWriter
as a normal POJO but that doesn't seem to be what the DocCollection
class is. So instead what was done was to just write it to JSON byte[]
then back to a Map
which was the "easiest" way.
Was looking to avoid this back and forth, but there were a few options. I tried using an ObjectMapper
to Map.class
but there is an error for unable to cast Instant due to a missing dependency for Jackson we need to introduce.
Java 8 date/time type
java.time.Instant not supported by default Issue
Other way is to introduce a some kind of toMap
method so that the TextWriter can write this as a generic Map
.
Another option which actually looks like the way we should go is I found that the DocCollection
class extends ZkNodeProps which implements MapWriter
. DocCollection
already overrides writeMap so we could just return this to the TextWriter! Unfortunately the ClusterStatus class does a bunch of JSON post processing such as Health
added to the Map
that the output is missing some things because of this postProcessCollectionJSON() method.
I am thinking we should refactor DocCollection to so we can just return this but the changes were much more drastic but may be worth it. Maybe in a different JIRA? This scope continues to creep with me adding improvement to NamedList
. Would be happy to refactor this and pick it up if you agree. Would clean up much more code and avoid this JSON processing for every collection iteration.
@@ -368,4 +341,77 @@ public static Map<String, Object> postProcessCollectionJSON(Map<String, Object> | |||
collection.put("health", Health.combine(healthStates).toString()); | |||
return collection; | |||
} | |||
|
|||
private class SolrCollectionProperiesIterator implements Iterator<NamedList<Object>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was hoping we wouldn't need a custom Iterator. We do need a method that takes in DocCollection (and some other context that is the same across collections) and returns a NamedList. With such a method, we can call it via streamOfDocCollection.map(collState -> theMethod(collState, routeKey, liveNodes, etc.).iterator()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah don't know why I didn't do this first. Changed it appropriately.
solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpClusterStateProvider.java
Outdated
Show resolved
Hide resolved
solr/solrj/src/java/org/apache/solr/client/solrj/impl/BaseHttpClusterStateProvider.java
Show resolved
Hide resolved
Iterator<Map<String, Object>> it = | ||
collectionStream | ||
.map( | ||
(collectionState) -> | ||
collectionPropsResponse( | ||
collectionState, | ||
collectionVsAliases, | ||
routeKey, | ||
liveNodes, | ||
requestedShards)) | ||
.iterator(); | ||
while (it.hasNext()) { | ||
Map<String, Object> props = it.next(); | ||
props.forEach( | ||
(key, value) -> { | ||
try { | ||
ew.put(key, value); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am thinking, if we can change up DocCollection in another PR, we can delete all those post processing functions and condense the response to this.
MapWriter collectionPropsWriter =
ew -> {
Iterator<DocCollection> it = collectionStream.iterator();
while (it.hasNext()) {
DocCollection collState = it.next();
ew.put(collState.getName(), collState);
}
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should mention Solr has two similar APIs, ClusterStatus & ColStatus. See org.apache.solr.handler.admin.ColStatus#getColStatus. It'd be awesome if there was a single method that takes a DocCollection (and some other params as needed) to produce a NamedList. At least ColStatus's code doesn't have the sad JSON round-trip. Maybe it's own PR or not as you wish.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. I'll take a look at this in a separate PR/Jira
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leave the round-trip serialization -- I didn't realize it was that way before.
Leave findRecursive be... not as easy as you thought and probably deserves its own issue.
solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
Outdated
Show resolved
Hide resolved
solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
Outdated
Show resolved
Hide resolved
solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
Outdated
Show resolved
Hide resolved
solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
Outdated
Show resolved
Hide resolved
solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
Outdated
Show resolved
Hide resolved
Iterator<Map<String, Object>> it = | ||
collectionStream | ||
.map( | ||
(collectionState) -> | ||
collectionPropsResponse( | ||
collectionState, | ||
collectionVsAliases, | ||
routeKey, | ||
liveNodes, | ||
requestedShards)) | ||
.iterator(); | ||
while (it.hasNext()) { | ||
Map<String, Object> props = it.next(); | ||
props.forEach( | ||
(key, value) -> { | ||
try { | ||
ew.put(key, value); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should mention Solr has two similar APIs, ClusterStatus & ColStatus. See org.apache.solr.handler.admin.ColStatus#getColStatus. It'd be awesome if there was a single method that takes a DocCollection (and some other params as needed) to produce a NamedList. At least ColStatus's code doesn't have the sad JSON round-trip. Maybe it's own PR or not as you wish.
solr/core/src/java/org/apache/solr/handler/admin/ClusterStatus.java
Outdated
Show resolved
Hide resolved
Set<String> shards = new HashSet<>(requestedShards); | ||
String name = clusterStateCollection.getName(); | ||
|
||
if (routeKey != null) { | ||
DocRouter router = clusterStateCollection.getRouter(); | ||
Collection<Slice> slices = router.getSearchSlices(routeKey, null, clusterStateCollection); | ||
for (Slice slice : slices) { | ||
requestedShards.add(slice.getName()); | ||
shards.add(slice.getName()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
something seems wrong to me here. If requestedShards is specified, we should use that. If routeKey is specified, we should use that (to compute the shards). Or neither (99% of users won't do either). But we shouldn't do both if for no other reason that it's confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could throw a SolrException(BAD_REQUEST)
higher up the stack if passed both shard
and _route_
. I can update the ref-docs to say you can only do one or the other. But this could break people who are doing both.
I also changed that logic into a single line and forEach
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend that but understand if you leave out of scope.
(I really don't think this is going to break anyone's current usage, BTW)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add it in when I come back for the refactor.
Map<String, Object> shards = (Map<String, Object>) collectionProps.get("shards"); | ||
for (Object nextShard : shards.values()) { | ||
Map<String, Object> shardMap = (Map<String, Object>) nextShard; | ||
Map<String, Object> replicas = (Map<String, Object>) shardMap.get("replicas"); | ||
for (Object nextReplica : replicas.values()) { | ||
Map<String, Object> replicaMap = (Map<String, Object>) nextReplica; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I almost want to cry just glancing at this.
This is the poster-child for why Java introduced "var". And there may be other approaches to improve it but that's the simplest.
(Yeah you didn't write it; I know)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll change those Maps
to var
but its another reason I think I'll come back to this. I could try to improve on this and I think its possible to remove a bunch of code here like buildResponseForCollection
and postProcessCollectionJSON
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
totally understood. In some old Solr code like this, there's always "and one more thing" we could/should do but ultimately snowballs the scope out of control. I leave it to you to do as you wish. Thank you for your contribution here; I didn't mean to get more out of you than you bargained for :-)
liveNodes, | ||
requestedShards)); | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does buildResponseForCollection throw IOException? That's suspicious. If you must, catch in there and throw a suitable exception like SolrException (which extends RuntimeException and is generally preferred within Solr over RE).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
buildResponseForCollection
doesn't throw the IOException
, the EntryWriter does but looks like there is actually a putNoEx
method that catches for us and that throws a generic RuntimeException
. I could throw a SolrException
there with better logging if you think its better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
putNoEx then
collectionStream.forEach( | ||
(collectionState) -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
friggin beautiful now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
putNoEx
removes the try/catch we can get it cleaner!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uh oh; a back-compat problem is dawning on me. Any SolrJ consumer of this API (especially using the default "javabin" format) is going to break here. The change to BaseHttpClusterStateProvider should have made this glaringly obvious but I overlooked its implication earlier (face-palm).
I wouldn't mind if we do this only for V2 (which doesn't yet have back-compatibility concerns) but V2 only has one collection status list returning API (/cluster) and I've debated against the very existence of /cluster for V2. There's not yet a proposal for an alternative. Furthermore V2 raises the bigger question of coalescing on a single DocCollection serialization method.
If we forge ahead anyway (not waiting for V2), we'll have to work around this compatibility. I had a couple ideas but whittled down to one:
- detect the response format in this handler. If it's Javabin, we write to a NamedList (or better, SimpleOrderedMap), defeating the point of this PR for that format. If JSON/XML is used, this PR will accomplish its goal. Need to write a little test for JSON that at least partially sanity checks the result. Such a test should pass without this PR.
https://issues.apache.org/jira/browse/SOLR-17582
Description
CLUSTERSTATUS
would aggregate and build the whole response including all collection properties into anNamedList
before finally passing it to the response writers. With many or thousands of collections, this can use up memory.Solution
Instead of returning a
NamedList
, return aMapWriter
. The Mapwriter will pull anIterator
from the documentStream and createSolrCollectionProperiesIterator
implementingiterator
andoverride
next()
to create and write the response in time, instead of writing the whole thing in memory.Tests
Fix appropriate tests that were using
NamedList
toMap
because of theMapWriter
.Checklist
Please review the following and check all that apply:
main
branch../gradlew check
.