Repository navigation
SOLR-18450: Relocate GET /api/cluster under /api/collections and return only the collections tree #4952
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-18450: Relocate GET /api/cluster under /api/collections and return only the collections tree #4952
Changes from all commits
d076904
4f6b305
4780ae3
6f1de94
82f7dc0
a2dfd80
d6e5497
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| title: | ||
| V2 GET /api/collections gains a `detailed=true` param returning the collections, shards, and | ||
| replicas tree (in `collectionsDetail`), without live nodes, aliases, or cluster properties. This | ||
| tree previously lived at GET /api/cluster in an earlier draft of this change; it was relocated to | ||
| /api/collections since it's collection data, not cluster-wide data -- `/api/cluster/nodes`, | ||
| `/api/aliases`, and `/api/cluster/properties` already cover the cluster-wide pieces. | ||
| type: changed | ||
| authors: | ||
| - name: Prithvi S | ||
| nick: iprithv | ||
| - name: Eric Pugh | ||
| links: | ||
| - name: SOLR-18450 | ||
| url: https://issues.apache.org/jira/browse/SOLR-18450 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,10 +20,12 @@ | |
| import java.util.Collection; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.function.BiConsumer; | ||
| import java.util.stream.Stream; | ||
| import org.apache.solr.client.api.util.SolrVersion; | ||
| import org.apache.solr.common.MapWriter; | ||
|
|
@@ -112,9 +114,10 @@ public void getClusterStatus(NamedList<Object> results, SolrVersion solrVersion) | |
| boolean withAliases = solrParams.getBool(ALIASES_PROP, includeAll); | ||
|
|
||
| List<String> liveNodes = null; | ||
| if (withLiveNodes || collection != null) { | ||
| // Replica state is cross-checked against live nodes whenever the collection tree is built, | ||
| // including when the caller asked for that tree without the live-node list itself. | ||
| if (withLiveNodes || withCollection) { | ||
| liveNodes = zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null); | ||
| // add live_nodes | ||
| if (withLiveNodes) clusterStatus.add("live_nodes", liveNodes); | ||
| } | ||
|
|
||
|
|
@@ -125,7 +128,7 @@ public void getClusterStatus(NamedList<Object> results, SolrVersion solrVersion) | |
|
|
||
| if (withCollection) { | ||
| assert liveNodes != null; | ||
| fetchClusterStatusForCollOrAlias(clusterStatus, liveNodes, aliases, solrVersion); | ||
| addCollectionStatuses(clusterStatus, liveNodes, aliases, solrVersion); | ||
| } | ||
|
|
||
| if (withAliases) { | ||
|
|
@@ -143,13 +146,49 @@ public void getClusterStatus(NamedList<Object> results, SolrVersion solrVersion) | |
| results.add("cluster", clusterStatus); | ||
| } | ||
|
|
||
| private void fetchClusterStatusForCollOrAlias( | ||
| /** | ||
| * Collections, shards, and replicas selected by this request's {@code collection}, {@code shard}, | ||
| * and {@code _route_} parameters. | ||
| * | ||
| * <p>Live nodes are read so replica state can be cross-checked, and aliases are read so a | ||
| * collection alias in {@code collection} can be resolved. Each collection still lists the aliases | ||
| * that point at it. The live-node list, the alias map, and cluster properties are not returned. | ||
| */ | ||
| public Map<String, Object> getCollectionStatuses() throws KeeperException, InterruptedException { | ||
| List<String> liveNodes = | ||
| zkStateReader.getZkClient().getChildren(ZkStateReader.LIVE_NODES_ZKNODE, null); | ||
| Aliases aliases = zkStateReader.getAliases(); | ||
| Map<String, Object> collections = new LinkedHashMap<>(); | ||
| forEachCollectionStatus( | ||
| liveNodes, aliases, (name, status) -> collections.put(name, copyForJackson(status))); | ||
| return collections; | ||
| } | ||
|
|
||
| private void addCollectionStatuses( | ||
| NamedList<Object> clusterStatus, | ||
| List<String> liveNodes, | ||
| Aliases aliases, | ||
| SolrVersion solrVersion) { | ||
| // Resolve the collection list now so a missing name fails the request, rather than during | ||
| // response writing. The per-collection JSON is still built while the response is written. | ||
| PreparedCollections prepared = prepareCollections(aliases); | ||
| if (solrVersion == null || solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. huh.... is this part of a rolling upgrade feature?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thsi is pre-existing, from SOLR-17582.. it streams collections as a MapWriter for SolrJ 9.9+, and falls back to a NamedList for older clients. not a rolling-upgrade feature of this change... just left that path alone. |
||
| MapWriter collectionPropsWriter = | ||
| ew -> prepared.forEach(liveNodes, (name, status) -> ew.putNoEx(name, status)); | ||
| clusterStatus.add("collections", collectionPropsWriter); | ||
| } else { | ||
| NamedList<Object> collectionProps = new SimpleOrderedMap<>(); | ||
| prepared.forEach(liveNodes, collectionProps::add); | ||
| clusterStatus.add("collections", collectionProps); | ||
| } | ||
| } | ||
|
|
||
| private void forEachCollectionStatus( | ||
| List<String> liveNodes, Aliases aliases, BiConsumer<String, Map<String, Object>> consumer) { | ||
| prepareCollections(aliases).forEach(liveNodes, consumer); | ||
| } | ||
|
|
||
| // read aliases | ||
| private PreparedCollections prepareCollections(Aliases aliases) { | ||
| Map<String, List<String>> collectionVsAliases = new HashMap<>(); | ||
| Map<String, List<String>> aliasVsCollections = aliases.getCollectionAliasListMap(); | ||
| for (Map.Entry<String, List<String>> entry : aliasVsCollections.entrySet()) { | ||
|
|
@@ -164,12 +203,6 @@ private void fetchClusterStatusForCollOrAlias( | |
| } | ||
|
|
||
| ClusterState clusterState = zkStateReader.getClusterState(); | ||
|
|
||
| String routeKey = solrParams.get(ShardParams._ROUTE_); | ||
| String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP); | ||
|
|
||
| Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) : null; | ||
|
|
||
| Stream<DocCollection> collectionStream; | ||
| if (collection == null) { | ||
| collectionStream = clusterState.collectionStream(); | ||
|
|
@@ -195,33 +228,53 @@ private void fetchClusterStatusForCollOrAlias( | |
| } | ||
| } | ||
|
|
||
| if (solrVersion == null || solrVersion.greaterThanOrEqualTo(SolrVersion.valueOf("9.9.0"))) { | ||
| MapWriter collectionPropsWriter = | ||
| ew -> { | ||
| collectionStream.forEach( | ||
| (collectionState) -> { | ||
| ew.putNoEx( | ||
| collectionState.getName(), | ||
| buildResponseForCollection( | ||
| collectionState, | ||
| collectionVsAliases, | ||
| routeKey, | ||
| liveNodes, | ||
| requestedShards)); | ||
| }); | ||
| }; | ||
| clusterStatus.add("collections", collectionPropsWriter); | ||
| } else { | ||
| NamedList<Object> collectionProps = new SimpleOrderedMap<>(); | ||
| collectionStream.forEach( | ||
| collectionState -> { | ||
| collectionProps.add( | ||
| collectionState.getName(), | ||
| buildResponseForCollection( | ||
| collectionState, collectionVsAliases, routeKey, liveNodes, requestedShards)); | ||
| }); | ||
| clusterStatus.add("collections", collectionProps); | ||
| String routeKey = solrParams.get(ShardParams._ROUTE_); | ||
| String shard = solrParams.get(ZkStateReader.SHARD_ID_PROP); | ||
| Set<String> requestedShards = (shard != null) ? Set.of(shard.split(",")) : null; | ||
| return new PreparedCollections( | ||
| collectionStream.toList(), collectionVsAliases, routeKey, requestedShards); | ||
| } | ||
|
|
||
| private final class PreparedCollections { | ||
| private final List<DocCollection> collections; | ||
| private final Map<String, List<String>> collectionVsAliases; | ||
| private final String routeKey; | ||
| private final Set<String> requestedShards; | ||
|
|
||
| private PreparedCollections( | ||
| List<DocCollection> collections, | ||
| Map<String, List<String>> collectionVsAliases, | ||
| String routeKey, | ||
| Set<String> requestedShards) { | ||
| this.collections = collections; | ||
| this.collectionVsAliases = collectionVsAliases; | ||
| this.routeKey = routeKey; | ||
| this.requestedShards = requestedShards; | ||
| } | ||
|
|
||
| private void forEach(List<String> liveNodes, BiConsumer<String, Map<String, Object>> consumer) { | ||
| for (DocCollection collectionState : collections) { | ||
| consumer.accept( | ||
| collectionState.getName(), | ||
| buildResponseForCollection( | ||
| collectionState, collectionVsAliases, routeKey, liveNodes, requestedShards)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Jersey serializes the v2 response with Jackson, which does not write Solr {@link MapWriter}s. | ||
| * Per-replica state is the one value still attached as a writer; turn it into plain JSON objects. | ||
| * Other fields are already maps and scalars. | ||
| */ | ||
| private static Map<String, Object> copyForJackson(Map<String, Object> status) { | ||
| Object prs = status.get("PRS"); | ||
| if (!(prs instanceof MapWriter)) { | ||
| return status; | ||
| } | ||
| Map<String, Object> copy = new LinkedHashMap<>(status); | ||
| copy.put("PRS", Utils.fromJSON(Utils.toJSON(prs))); | ||
| return copy; | ||
| } | ||
|
|
||
| private void addAliasMap(Aliases aliases, NamedList<Object> clusterStatus) { | ||
|
|
||
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.
is there also a registerV2 Api method on this guy which can be removed?
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.
no registerV2 on this class :) only removed old GET /cluster @ EndPoint that forwarded to v1 CLUSTERSTATUS. kept POST /cluster (set-ratelimiter).